linux网络编程之socket(三):最简单的回射客户/服务器程序、time_wait 状态

下面通过最简单的客户端/服务器程序的实例来学习socket API。

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

/*************************************************************************
    > File Name: echoser.c
    > Author: Simba
    > Mail: dameng34@163.com
    > Created Time: Fri 01 Mar 2013 06:15:27 PM CST
 ************************************************************************/

#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));

    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;
}

下面介绍程序中用到的socket API,这些函数都在sys/socket.h中。

int socket(int family, int type, int protocol); socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以像读写文件一样用read/write在网络上收发数据,如果socket()调用出错则返回-1。对于IPv4,family参数指定为AF_INET。对于TCP协议,type参数指定为SOCK_STREAM,表示面向流的传输协议。如果是UDP协议,则type参数指定为SOCK_DGRAM,表示面向数据报的传输协议。protocol参数的介绍从略,指定为0即可。

int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen); 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用bind绑定一个固定的网络地址和端口号。bind()成功返回0,失败返回-1。 bind()的作用是将参数sockfd和myaddr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号。struct sockaddr *是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度。我们的程序中对myaddr参数是这样初始化的:

memset(&servaddr, 0, sizeof(servaddr));     servaddr.sin_family = AF_INET;     servaddr.sin_port = htons(5188);     servaddr.sin_addr.s_addr = htonl(INADDR_ANY); 

首先将整个结构体清零(也可以用bzero函数),然后设置地址类型为AF_INET,网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址,端口号为5188。

int listen(int sockfd, int backlog); 典型的服务器程序可以同时服务于多个客户端,当有客户端发起连接时,服务器调用的accept()返回并接受这个连接,如果有大量的客户端发起连接而服务器来不及处理,尚未accept的客户端就处于连接等待状态,listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接等待状态,如果接收到更多的连接请求就忽略。listen()成功返回0,失败返回-1。

int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen); 三方握手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。cliaddr是一个传出参数,accept()返回时传出客户端的地址和端口号。addrlen参数是一个传入传出参数(value-result argument),传入的是调用者提供的缓冲区cliaddr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给cliaddr和addrlen参

数传NULL,表示不关心客户端的地址。

在上面的程序中我们通过peeraddr打印连接上来的客户端ip和端口号。

在while循环中从accept返回的文件描述符conn读取客户端的请求,然后直接回射回去。

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

/*************************************************************************
    > File Name: echoclic
    > Author: Simba
    > Mail: dameng34@163.com
    > Created Time: Fri 01 Mar 2013 06:15:27 PM CST
 ************************************************************************/


#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");


    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(),内核会自动给服务器分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦。

int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen); 客户端需要调用connect()连接服务器,connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址。connect()成功返回0,出错返回-1。

先编译运行服务器:

simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ ./echoser

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

simba@ubuntu:~$ netstat -anp | grep 5188 (Not all processes could be identified, non-owned process info  will not be shown, you would have to be root to see it all.) tcp        0      0 0.0.0.0:5188            0.0.0.0:*               LISTEN      4425/echoser  

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

simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ ./echocli

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

simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ ./echoser recv connect ip=127.0.0.1 port=59431

可见客户端的端口号是自动分配的。

再次netstat 一下

simba@ubuntu:~$ netstat -anp | grep 5188 (Not all processes could be identified, non-owned process info  will not be shown, you would have to be root to see it all.) tcp        0      0 0.0.0.0:5188            0.0.0.0:*               LISTEN      4425/echoser     tcp        0      0 127.0.0.1:59431         127.0.0.1:5188          ESTABLISHED 4852/echocli     tcp        0      0 127.0.0.1:5188          127.0.0.1:59431         ESTABLISHED 4425/echoser  

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

上面第一行即echoser.c 中的listenfd;第二行即echocli 中的sock; 第三行即echoser.c 中的conn。4425和4852分别是进程id。

现在来做个测试,先把echoser.c 中40~42行的代码注释起来。

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

simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ ./echoser bind error: Address already in use

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

simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ netstat -anp | grep 5188 (Not all processes could be identified, non-owned process info  will not be shown, you would have to be root to see it all.) tcp        0      0 127.0.0.1:5188          127.0.0.1:37381         FIN_WAIT2   -                tcp        1      0 127.0.0.1:37381         127.0.0.1:5188          CLOSE_WAIT  2302/echocli    

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

Many implementations prevent this infinite wait in the FIN_WAIT_2 state as follows: If the application that does the active close does a complete close, not a half-close indicating that it expects to receive data, a timer is set. If the connection is idle when the timer expires, TCP moves the connection into the CLOSED state. In Linux, the variable net.ipv4.tcp_fin_timeout can be adjusted to control the number of seconds to which the timer is set. Its default value is 60s.

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

simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ netstat -anp | grep 5188 (No info could be read for "-p": geteuid()=1000 but you should be root.) tcp        0      0 127.0.0.1:5188          127.0.0.1:37381         TIME_WAIT   -                simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ ./echoser bind error: Address already in use

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

MSL: the maximum amount of time any segment can exist in the network before being discarded. We know that this time limit is bounded, because TCP segments are transmitted as IP datagrams, and the IP datagram has the TTL field or Hop Limit field that limits its effective lifetime.

Given the MSL value for an implementation, the rule is: When TCP performs an active close and sends the final ACK, that connection must stay in the TIME_WAIT state for twice the MSL. This lets TCP resend the final ACK in case it is lost. The final ACK is resent not because the TCP retransmits ACKs (they do not consume sequence numbers and are not retransmitted by TCP), but because the other side will retransmit its FIN (which does consume a sequence number). Indeed, TCP will always retransmit FINs until it receives a final ACK.

在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描述符。将原来注释的40~42行代码打开,问题解决。

参考:

  1. 《Linux C 编程一站式学习》
  2. 《TCP/IP详解 卷一》
  3. 《UNP》
  4. http://vincent.bernat.im/en/blog/2014-tcp-time-wait-state-linux.html
  5. http://www.serverframework.com/asynchronousevents/2011/01/time-wait-and-its-design-implications-for-protocols-and-scalable-servers.html

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏逸鹏说道

C# 实现发送手机短信

现在很多网站都是短信发送的功能,怎么实现的呢。对于个人站长来说的话,通过使用SMS短信通API接口相对比较划算和简单。那怎么实现呢,步骤如下: 1. 从网上(h...

5814
来自专栏安智客

密码技术之消息认证码

上一节我们知道消息的完整性保护需要通过单向散列函数来保证。但是光完整性还不够,我们还有确认消息的来源必须是正确的发送者。

1145
来自专栏安富莱嵌入式技术分享

【RL-TCPnet网络教程】第38章 TFTP简单文件传输基础知识

本章节为大家讲解TFTP(Trivial File Transfer Protocol,简单文件传输协议)的基础知识,方便后面章节的实战操作。

702
来自专栏jouypub

CentOS上安装remix-ide错误及解决方法

remix-ide安装文档:https://github.com/ethereum/remix-ide

530
来自专栏向治洪

AndFix使用感想

AndFix已经使用了一段时间了,但是到AndFix上看了一下,最近2个月都没有更新代码了,有141个issues和3个pull request没人处理,其...

1996
来自专栏desperate633

替换空格

692
来自专栏Golang语言社区

优化Linux的内核参数来提高服务器并发处理能力

PS:在服务器硬件资源额定有限的情况下,最大的压榨服务器的性能,提高服务器的并发处理能力,是很多运维技术人员思考的问题。要提高Linux系统下的负载能力,可以使...

1984
来自专栏kwcode

C# 读取指定文件夹下所有文件

#region 读取文件 //返回指定目录中的文件的名称(绝对路径) string[] files = S...

2647
来自专栏任浩强的运维生涯

优化内核参数提高服务器的并发处理能力

PS:在服务器硬件资源额定有限的情况下,最大的压榨服务器的性能,提高服务器的并发处理能力,是很多运维技术人员思考的问题。要提高Linux系统下的负载能力,可以使...

3988
来自专栏前端架构

IP数据包的传输全过程详解—数据是如何在tcp/ip各层封装?

 我们知道现在的互联网中使用的TCP/IP协议是基于,OSI(开放系统互联)的七层参考模型的,(虽然不是完全符合)从上到下分别为 应用层 表示层 会话层 传输层...

954

扫码关注云+社区