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

来源:互联网 发布:外国网民评论中国知乎 编辑:程序博客网 时间:2024/05/16 09:49

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 plaincopyprint?
  1. huangcheng@ubuntu:~$./serv  

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

[cpp] view plaincopyprint?
  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 plaincopyprint?
  1. huangcheng@ubuntu:~$ ./cli  

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

[cpp] view plaincopyprint?
  1. huangcheng@ubuntu:~$ ./serv  
  2. recv connect ip=127.0.0.1 port=42107  

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

[cpp] view plaincopyprint?
  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 plaincopyprint?
  1. huangcheng@ubuntu:~$ ./serv  
  2. bind error: Address already in use  
     这是因为,虽然server的应用程序终止了,但TCP协议层的连接并没有完全断开,因此不能再次监听同样的server端口。我们用netstat命令查看一下:
[cpp] view plaincopyprint?
  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 plaincopyprint?
  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 plaincopyprint?
  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 plaincopyprint?
  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 plaincopyprint?
  1. huangcheng@ubuntu:~$ ./cli  
  2. host 127.0.0.1:42107  
  3. huangcheng  
  4. huangcheng  
  5. ctt  
  6. ctt  

0 0
原创粉丝点击