【手把手教你写服务器】TCP状态转换、TIME_WAIT状态、SO_REUSEADDR选项

文章目录

  • 1.TCP状态转换
  • 2.TIME_WAIT状态
  • 3.SO_REUSEADDR选项

1.TCP状态转换

关于TCP状态转换,自行查阅《UNIX网络编程_卷1_套接字联网API_第3版》第二章第六节。

【手把手教你写服务器】TCP状态转换、TIME_WAIT状态、SO_REUSEADDR选项_第1张图片

2.TIME_WAIT状态

关于TIME_WAIT状态,自行查阅《UNIX网络编程_卷1_套接字联网API_第3版》第二章第七节。

server.c:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define SERV_PORT 9000 // 本服务器要监听的端口号,一般1024以下的端口很多都是周知端口,所以这里采用1024之后的数字做端口号

int main(int argc, char* const* argv)
{
    int listenfd = socket(AF_INET, SOCK_STREAM, 0); // 创建服务器的socket套接字(文件描述符)

    struct sockaddr_in serv_addr; // 服务器的地址结构体
    memset(&serv_addr, 0, sizeof(serv_addr));
    
    // 设置本服务器要监听的地址和端口,这样客户端才能连接到该地址和端口并发送数据
    serv_addr.sin_family = AF_INET; // 选择协议族为IPV4
    serv_addr.sin_port = htons(SERV_PORT); // 绑定我们自定义的端口号,客户端程序和服务器程序通讯时,就要往这个端口连接和传送数据
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听本地所有的IP地址,INADDR_ANY表示一个服务器上所有的网卡(服务器可能不止一个网卡),多个本地ip地址都进行绑定端口号,进行侦听

    int result;

    result = bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); // 绑定服务器地址结构体
    
    if (result  == -1)
    {
        char* perrorinfo = strerror(errno); 
        printf("bind返回的值为%d,错误码为:%d,错误信息为:%s;\n", result, errno, perrorinfo);
        return -1;
    }

    result = listen(listenfd, 32); // 第二个参数表示服务器可以积压的未处理完的连入请求总个数,客户端来一个未连入的请求,请求数+1,连入请求完成、c/s之间进入正常通讯后,请求数-1
    
    if (result == -1)
    {
        char* perrorinfo = strerror(errno); 
        printf("listen返回的值为%d,错误码为:%d,错误信息为:%s;\n", result, errno, perrorinfo);
        return -1;
    }

    int connfd;
    
    const char* pcontent = "I sent sth to client!\n"; // 指向常量字符串区的指针
    
    for (;;)
    {
        // 卡在这里,等客户端连接,客户端连入后,该函数走下去
        // 注意这里返回的是一个新的socket(connfd),后续本服务器就用connfd和客户端之间收发数据,而原有的lisenfd依旧用于继续监听其他连接        
        connfd = accept(listenfd, (struct sockaddr*)NULL, NULL);

        // 发送数据包给客户端
        write(connfd, pcontent, strlen(pcontent)); // 注意第一个参数是accept()返回的connfd套接字
        
        printf("本服务器给客户端发送了一串字符~~~~~~~~~~~!\n");
        
        // 只给客户端发送一个信息,然后直接关闭套接字连接
        close(connfd);
    }
    
    close(listenfd); // 实际上本范例走不到这里

    return 0;
}

运行服务器程序,用 netstat -anp | grep -E 'State|9000' 命令观察到监听端口一直处在 LISTEN 状态,我们用两个客户端连接到服务器,服务器给每个客户端发送一串字符 “I sent sth to client!\n” 并关闭客户端,虽然这两个连接被 close 掉了,但是产生了两条 TIME_WAIT 状态的信息。

此时,我们杀掉服务器程序再重新启动,就会启动失败,bind() 函数返回失败。

3.SO_REUSEADDR选项

server.c:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define SERV_PORT 9000 // 本服务器要监听的端口号,一般1024以下的端口很多都是周知端口,所以这里采用1024之后的数字做端口号

int main(int argc, char* const* argv)
{
    int listenfd = socket(AF_INET, SOCK_STREAM, 0); // 创建服务器的socket套接字(文件描述符)

    struct sockaddr_in serv_addr; // 服务器的地址结构体

    memset(&serv_addr, 0, sizeof(serv_addr));
    
    // 设置本服务器要监听的地址和端口,这样客户端才能连接到该地址和端口并发送数据
    serv_addr.sin_family = AF_INET; // 选择协议族为IPV4
    serv_addr.sin_port = htons(SERV_PORT); // 绑定我们自定义的端口号,客户端程序和服务器程序通讯时,就要往这个端口连接和传送数据
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听本地所有的IP地址,INADDR_ANY表示一个服务器上所有的网卡(服务器可能不止一个网卡),多个本地ip地址都进行绑定端口号,进行侦听

    // setsockopt():设置一些套接字参数选项
    // 第二个参数:表示级别,和第三个参数配套使用,也就是说,如果第三个参数确定了,第二个参数也就确定了
    // 第三个参数:允许重用本地地址
    int reuseaddr = 1; // 开启
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (const void *) &reuseaddr, sizeof(reuseaddr)) == -1)
    {
        char* perrorinfo = strerror(errno);
        printf("setsockopt(SO_REUSEADDR)返回值为%d,错误码为:%d,错误信息为:%s;\n", -1, errno, perrorinfo);
    }

    int result;

    result = bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); // 绑定服务器地址结构体
    
    if (result  == -1)
    {
        char* perrorinfo = strerror(errno); 
        printf("bind返回的值为%d,错误码为:%d,错误信息为:%s;\n", result, errno, perrorinfo);
        return -1;
    }
    
    result = listen(listenfd, 32); // 第二个参数表示服务器可以积压的未处理完的连入请求总个数,客户端来一个未连入的请求,请求数+1,连入请求完成、c/s之间进入正常通讯后,请求数-1
    
    if (result == -1)
    {
        char* perrorinfo = strerror(errno); 
        printf("listen返回的值为%d,错误码为:%d,错误信息为:%s;\n", result, errno, perrorinfo);
        return -1;
    }

    int connfd;

    const char* pcontent = "I sent sth to client!\n"; // 指向常量字符串区的指针
    
    for (;;)
    {
        // 卡在这里,等客户端连接,客户端连入后,该函数走下去
        // 注意这里返回的是一个新的socket(connfd),后续本服务器就用connfd和客户端之间收发数据,而原有的lisenfd依旧用于继续监听其他连接        
        connfd = accept(listenfd, (struct sockaddr*)NULL, NULL);

        // 发送数据包给客户端
        write(connfd, pcontent, strlen(pcontent)); // 注意第一个参数是accept()返回的connfd套接字

        printf("本服务器给客户端发送了一串字符~~~~~~~~~~~!\n");
        
        // 只给客户端发送一个信息,然后直接关闭套接字连接
        close(connfd);
    }

    close(listenfd); // 实际上本范例走不到这里
    
    return 0;
}

setsockopt(SO_REUSEADDR):用在服务器端,socket() 创建之后,bind() 之前。

SO_REUSEADDR 的能力:

  1. SO_REUSEADDR 允许启动一个监听服务器并捆绑其端口,即使以前建立的将端口用作它们的本地端口的连接仍旧存在。也就是说,即便 TIME_WAIT 状态存在,服务器 bind() 也能成功。
  2. SO_REUSEADDR 允许同一个端口上启动同一个服务器的多个实例,只要每个实例捆绑一个不同的本地 IP 地址即可。
  3. SO_REUSEADDR 允许单个进程捆绑同一个端口到多个套接字,只要每次捆绑指定不同的本地 IP 地址即可。
  4. SO_REUSEADDR 允许完全重复的绑定:当一个 IP 地址和端口已经绑定到某个套接字上时,如果传输协议支持,同样的 IP 地址和端口还可以绑定到另一个套接字上,一般来说本特性仅支持 UDP 套接字。
  5. 所有 TCP 服务器都应该指定本套接字选项,以防止当套接字处于 TIME_WAIT 时 bind() 失败的情形出现。

你可能感兴趣的:(nginx,TCP,TIME_WAIT,SO_REUSEADDR)