首页 > 编程语言 >深入研究socket编程(1)-----socket之TCP回射服务器/客户端程序

深入研究socket编程(1)-----socket之TCP回射服务器/客户端程序

时间:2022-12-15 20:08:59浏览次数:34  
标签:127.0 socket 0.1 TCP 客户端程序 sizeof include servaddr


unix环境高级编程-------socket(套接字)中对socket编程有了初步的了解,在本篇以及后续的博客中来深入探讨各种实例以及更多的socket编程技术。



                            

serv.c 程序的功能是从客户端读取字符然后直接回射回去:



#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<string.h>

#define ERR_EXIT(m) \
do { \
perror(m); \
exit(EXIT_FAILURE); \
} while (0)


int main(void)
{
int listenfd; //被动套接字(文件描述符),即只可以accept
if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
// listenfd = socket(AF_INET, SOCK_STREAM, 0)
ERR_EXIT("socket error");

struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(5188);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
/* servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); */
/* inet_aton("127.0.0.1", &servaddr.sin_addr); */

int on = 1;
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
ERR_EXIT("setsockopt error");

if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
ERR_EXIT("bind error");

if (listen(listenfd, SOMAXCONN) < 0) //listen应在socket和bind之后,而在accept之前
ERR_EXIT("listen error");

struct sockaddr_in peeraddr; //传出参数
socklen_t peerlen = sizeof(peeraddr); //传入传出参数,必须有初始值
int conn; // 已连接套接字(变为主动套接字,即可以主动connect)
if ((conn = accept(listenfd, (struct sockaddr *)&peeraddr, &peerlen)) < 0)
ERR_EXIT("accept error");
printf("recv connect ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));
struct sockaddr_in localaddr;
char serv_ip[20];
socklen_t local_len = sizeof(localaddr);
memset(&localaddr, 0, sizeof(localaddr));
if( getsockname(conn,(struct sockaddr *)&localaddr,&local_len) != 0 )
ERR_EXIT("getsockname error");
inet_ntop(AF_INET, &localaddr.sin_addr, serv_ip, sizeof(serv_ip));
printf("host %s:%d\n", serv_ip, ntohs(localaddr.sin_port));

char recvbuf[1024];
while (1)
{
memset(recvbuf, 0, sizeof(recvbuf));
int ret = read(conn, recvbuf, sizeof(recvbuf));
fputs(recvbuf, stdout);
write(conn, recvbuf, ret);
}

close(conn);
close(listenfd);

return 0;
}



cli.c 的作用是从标准输入得到一行字符,然后发送给服务器后从服务器接收,再打印在标准输出:

#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<string.h>


#define ERR_EXIT(m) \
do { \
perror(m); \
exit(EXIT_FAILURE); \
} while (0)




int main(void)
{
int sock;
if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
// listenfd = socket(AF_INET, SOCK_STREAM, 0)
ERR_EXIT("socket error");


struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(5188);
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
/* inet_aton("127.0.0.1", &servaddr.sin_addr); */

if (connect(sock, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
ERR_EXIT("connect error");
struct sockaddr_in localaddr;
char cli_ip[20];
socklen_t local_len = sizeof(localaddr);
memset(&localaddr, 0, sizeof(localaddr));
if( getsockname(sock,(struct sockaddr *)&localaddr,&local_len) != 0 )
ERR_EXIT("getsockname error");
inet_ntop(AF_INET, &localaddr.sin_addr, cli_ip, sizeof(cli_ip));
printf("host %s:%d\n", cli_ip, ntohs(localaddr.sin_port));

char sendbuf[1024] = {0};
char recvbuf[1024] = {0};
while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{

write(sock, sendbuf, strlen(sendbuf));
read(sock, recvbuf, sizeof(recvbuf));


fputs(recvbuf, stdout);

memset(sendbuf, 0, sizeof(sendbuf));
memset(recvbuf, 0, sizeof(recvbuf));
}


close(sock);


return 0;
}



     由于客户端不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配。注意,客户端不是不允许调用bind(),只是没有必要调用bind()固定一个端口号,服务器也不是必须调用bind(),但如果服务器不调用bind(),内核会自动给服务器分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦。


     先编译运行服务器:



[cpp]  ​​view plain​​ ​​copy​​ ​​print​​ ​​?​​



  1. huangcheng@ubuntu:~$./serv  



     然后在另一个终端里用netstat命令查看:




[cpp]  ​​view plain​​ ​​copy​​ ​​print​​ ​​?​​



  1. huangcheng@ubuntu:~$ netstat -anp | grep 5188  
  2. (并非所有进程都能被检测到,所有非本用户的进程信息将不会显示,如果想看到所有信息,则必须切换到 root 用户)  
  3. tcp        0      0 0.0.0.0:5188            0.0.0.0:*               LISTEN      2998/serv  


     

     可以看到server程序监听5188端口,IP地址还没确定下来。现在编译运行客户端:



[cpp]  ​​view plain​​ ​​copy​​ ​​print​​ ​​?​​



  1. huangcheng@ubuntu:~$ ./cli  



     回到server所在的终端,看看server的输出:




[cpp]  ​​view plain​​ ​​copy​​ ​​print​​ ​​?​​



  1. huangcheng@ubuntu:~$ ./serv  
  2. recv connect ip=127.0.0.1 port=42107  



     可见客户端的端口号是自动分配的。再次netstat 一下:



[cpp]  ​​view plain​​ ​​copy​​ ​​print​​ ​​?​​



  1. huangcheng@ubuntu:~$ netstat -anp | grep 5188  
  2. (并非所有进程都能被检测到,所有非本用户的进程信息将不会显示,如果想看到所有信息,则必须切换到 root 用户)  
  3. tcp        0      0 0.0.0.0:5188            0.0.0.0:*               LISTEN      2998/serv     
  4. tcp        0      0 127.0.0.1:5188          127.0.0.1:42107         ESTABLISHED 2998/serv     
  5. tcp        0      0 127.0.0.1:42107         127.0.0.1:5188          ESTABLISHED 3198/cli      



      应用程序中的一个socket文件描述符对应一个socket pair,也就是源地址:源端口号和目的地址:目的端口号,也对应一个TCP连接。

     上面第一行即serv.c 中的listenfd;第二行即serv.c 中的sock; 第三行即cli 中的conn。2998和3198分别是进程id。

     现在来做个测试,先serv.c中的把33~35行的代码注释掉。

     首先启动server,然后启动client,然后用Ctrl-C使server终止,这时马上再运行server,结果是:



[cpp]  ​​view plain​​ ​​copy​​ ​​print​​ ​​?​​



  1. huangcheng@ubuntu:~$ ./serv  
  2. bind error: Address already in use  


     这是因为,虽然server的应用程序终止了,但TCP协议层的连接并没有完全断开,因此不能再次监听同样的server端口。我们用netstat命令查看一下:

[cpp]  ​​view plain​​ ​​copy​​ ​​print​​ ​​?​​



  1. huangcheng@ubuntu:~$ netstat -anp | grep 5188  
  2. (并非所有进程都能被检测到,所有非本用户的进程信息将不会显示,如果想看到所有信息,则必须切换           到 root 用户)  
  3. tcp        0      0 127.0.0.1:5188          127.0.0.1:42108         FIN_WAIT2   -                        
  4. tcp        1      0 127.0.0.1:42108         127.0.0.1:5188          CLOSE_WAIT  3260/cli                 


     server终止时,socket描述符会自动关闭并发FIN段给client,client收到FIN后处于CLOSE_WAIT状态,但是client并没有终止,也没有关闭socket描述符,因此不会发FIN给server,因此server的TCP连接处于FIN_WAIT2状态。

     现在用Ctrl-C把client也终止掉,再观察现象:



[cpp]  ​​view plain​​ ​​copy​​ ​​print​​ ​​?​​



  1. huangcheng@ubuntu:~$ netstat -anp | grep 5188  
  2. (并非所有进程都能被检测到,所有非本用户的进程信息将不会显示,如果想看到所有信息,则必须切换到 root 用户)  
  3. tcp        0      0 127.0.0.1:5188          127.0.0.1:42108         TIME_WAIT   -  



[cpp]  ​​view plain​​ ​​copy​​ ​​print​​ ​​?​​



  1. huangcheng@ubuntu:~$ ./serv  
  2. bind error: Address already in use  



     client终止时自动关闭socket描述符,server的TCP连接收到client发的FIN段后处于TIME_WAIT状态。TCP协议规定,主动关闭连接的一方要处于TIME_WAIT状态,等待两个MSL(maximumsegment lifetime)的时间后才能回到CLOSED状态,需要有MSL 时间的主要原因是在这段时间内如果最后一个ack段没有发送给对方,则可以重新发送。因为我们先Ctrl-C终止了server,所以server是主动关闭连接的一方,在TIME_WAIT期间仍然不能再次监听同样的server端口。MSL在RFC1122中规定为两分钟,但是各操作系统的实现不同,在Linux上一般经过半分钟后就可以再次启动server了。至于为什么要规定TIME_WAIT的时间请大家参考UNP 2.7节。

     在server的TCP连接没有完全断开之前不允许重新监听是不合理的,因为,TCP连接没有完全断开指的是connfd(127.0.0.1:5188)没有完全断开,而我们重新监听的是listenfd(0.0.0.0:5188),虽然是占用同一个端口,但IP地址不同,connfd对应的是与某个客户端通讯的一个具体的IP地址,而listenfd对应的是wildcard address。解决这个问题的方法是使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1表示允许创建端口号相同但IP地址不同的多个socket描述符。将原来注释的33~35行代码打开,问题解决。

     先运行服务器,在运行客户端:



[cpp]  ​​view plain​​ ​​copy​​ ​​print​​ ​​?​​



  1. huangcheng@ubuntu:~$ ./serv  
  2. recv connect ip=127.0.0.1 port=42107  
  3. host 127.0.0.1:5188  
  4. huangcheng  
  5. ctt  



[cpp]  ​​view plain​​ ​​copy​​ ​​print​​ ​​?​​



  1. huangcheng@ubuntu:~$ ./cli  
  2. host 127.0.0.1:42107  
  3. huangcheng  
  4. huangcheng  
  5. ctt  
  6. ctt  



标签:127.0,socket,0.1,TCP,客户端程序,sizeof,include,servaddr
From: https://blog.51cto.com/u_15133569/5945870

相关文章

  • 深入研究socket编程(5)——I/O复用的高级应用
    高级应用一:非阻塞connectconnect系统调用的man手册中有如下的一段内容:[cpp] ​​viewplain​​​​copy​​​​printEINPROGRESS  for ......
  • 进程间通信-socketpair
    最近在看libcontainer中nsexec.c的实现,看到init进程的parent与child、grandchild之间的双工通信使用了socketpair。socketpair的使用与fifo类似,在不具名的情况下可以实现父......
  • vue Socket-io使用
    api文档:https://socket.io/docs/v4/client-api/只需要下载它socket.io-client最新版本,可以不用vue-socket-io"socket.io-client":"^4.5.4",连接socketimport{io......
  • Java Socket网络编程
    1.TCP流式SocketTCP是TCP/IP体系结构中位于传输层的面向连接的协议,提供可靠的字节流传输。通信双方需要建立连接,发送端的所有数据按顺序发送,接受端按顺序接收。......
  • 10. 图文并茂解释TCP/IP 3次握手4次断开。
      三次握手总结三次握手是TCP连接的建立过程。在握手之前,主动打开连接的客户端结束CLOSE阶段,被动打开的服务器也结束CLOSE阶段,并进入LISTEN阶段。随后进入......
  • unix网络编程2.6——高并发服务器(七)基于epoll&&reactor实现web_socket服务器
    目录系列文章unix网络编程1.1——TCP协议详解(一)unix网络编程2.1——高并发服务器(一)基础——io与文件描述符、socket编程与单进程服务端客户端实现unix网络编程2.2——高并......
  • 基于Java+Swing+Socket实现泡泡堂游戏
    @目录一、功能展示1.游戏登陆2.房间3.对战二、代码展示三、其他系统四、获取源码前言《泡泡堂》是由韩国游戏公司Nexon开发的一款休闲游戏(CasualGame),于2003年在中国大陆......
  • 玩转 Go 生态|Hertz WebSocket 扩展简析
    WebSocket是一种可以在单个TCP连接上进行全双工通信,位于OSI模型的应用层。WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据......
  • websocket携带jwt token
    在websocket中,目前未提供修改请求头字段的方法,不过可以借助于“Sec-WebSocket-Protocol”,将token放入请求头中,后端收到请求后,从请求头中取得token做校验。即:在前端websock......
  • socket的基本函数
    socket的基本操作:(1)socket()函数:(2)bind()函数:(3)listen(),connect()函数;(4)accept()函数;(5)socket中的发送与接收函数:(6)close()函数:(7)服务器上调用socket......