unix网络编程2.8——高并发服务器(八)unix网络编程系统调用与网络协议栈

目录

系列文章

阅读本文需要先阅读下面的文章:

unix网络编程1.1——TCP协议详解(一)

unix网络编程2.1——高并发服务器(一)基础——io与文件描述符、socket编程与单进程服务端客户端实现

unix网络编程2.2——高并发服务器(二)多进程与多线程实现

unix网络编程2.3——高并发服务器(三)多路IO复用之select

unix网络编程2.4——高并发服务器(四)epoll基础篇

unix网络编程2.5——高并发服务器(五)epoll进阶篇——基于epoll实现reactor

unix网络编程2.6——高并发服务器(六)基于epoll&&reactor实现http服务器

unix网络编程2.7——高并发服务器(七)基于epoll&&reactor实现web_socket服务器

1. 网络编程关注的问题

1.1 连接建立

1.2 连接断开

1.3 消息到达

1.4 消息发送

2. 网络io职责

2.1 操作io

2.1.1 阻塞io和非阻塞io

  • 阻塞在网络线程/进程;
  • 连接的fd阻塞属性决定了io函数是否阻塞;
  • 具体差异:io函数在数据未就绪时是否立刻返回;
// 默认情况下,fd是阻塞的,设置非阻塞的方法如下:
int flag = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flag | O_NONBLOCK);
  • 进行网络编程时,注意区分用户空间和内核空间。在内核空间中,读和写等操作对应有read buf和write buf,当我们使用fd去进行读或写(系统调用)时,比如read时,此时会通过这个fd去判断read buf是否可读,是否有数据;比如write时,此时会通过fd去判断write buf是否可写,是否还有空间。
    image

  • 如果是阻塞io,进行read时,会先去检测内核中read buf是否可读,是否有数据,如果没有则阻塞在这里,等待网卡驱动发来数据,然后将数据从内核空间拷贝到用户空间;

  • 如果是非阻塞io,进度read时,同理去检查内核中read buf是否可读,是否有数据,如果没有则立刻返回(返回错误码: EWOULDBLOCK)。
    image

2.1.2 非阻塞io处理方式

既有检测也有操作功能,只能检测一条连接,一个io

2.1.2.1 连接建立

  1. connect
    既有检测功能,又有操作功能
    一般客户端向服务端发起连接;服务端作为客户端,去连接数据库等情况;
    EINPROGRESS(正在建立)
    EISCONN(已经建立)

  2. listen(fd, backlog)
    服务端正常监听某个ip和port,等待客户端的连接——3次握手的过程
    backlog——半连接队列的长度
    在未调用accept之前,服务端能处理最多的未处理的连接个数(存在了这个队列里)
    image

  3. accept
    既有检测功能,又有操作功能
    检测功能:检测全连接队列里是否有未处理的连接信息
    操作io的功能:如果检测有连接信息,会把连接信息从全连接队列取出来,从而生成客户端fd,和获取客户端的ip和port(连接五元组
    int cfd = accept(listenfd, addr, sz);

2.1.2.2 连接断开

  1. 主动
    服务端主动断开连接
    close回收资源(fd)
    shutdown可以关闭读端,或者关闭写端
  2. 被动
    客户端主动断开连接,服务端应该怎么处理
    read返回值 == 0,说明服务端的读端关闭(此时服务端还可以调用write)
    write返回值 == -1 && errno == EPIPE,说明服务端的写端关闭(可能是客户端主动把读端关闭)

2.1.2.3 连接到达

read
EWOULDBLOCK,如果read返回值== -1,并且EWOULDBLOCK,说明内核空间read buf为空
EINTR,说明被中断打断了

2.1.2.4 消息发送

write
EWOULDBLOCK
EINTR

2.2 检测io

不会去操作具体的io,只检测io的就绪状态——io多路复用,可以同时检测多个io的就绪状态,多条连接状态

连接建立:socket -> bind & listen -> io多路复用检测io状态 -> io读事件就绪 -> accept只操作io,不检测
连接断开(epoll):
EPOLLRDHUP: 服务端读端关闭
EPOLLHUP: 服务端读写端都关闭
消息到达:监听客户端fd的读事件,EPOLLIN -> io读事件就绪 -> read只操作io,不检测。内核read buf有数据了
消息发送:监听客户端fd的写事件,EPOLLOUT -> IO写事件就绪 -> write只操作io,不检测。内核write buf可写,还有空间

3. reactor

对io的操作,转换为对事件的处理;异步事件的机制,比如我想对一个fd进行读,则我不会直接读,而是去监听其读事件,等待事件触发(异步回调)

3.1 io多路复用——epoll

epoll原理图
image

3.1.1 epoll 相关api

调用 epoll_create 会创建一个 epoll 对象;调用
epoll_ctl 添加到 epoll 中的事件都会与网卡驱动程序建立回
调关系,相应事件触发时会调用回调函数
( ep_poll_callback ),将触发的事件拷贝到 rdlist 双向
链表中;调用 epoll_wait 将会把 rdlist 中就绪事件拷贝到
用户态中;

epoll_wait参数2,是一个用户态的数组,用来拷贝就绪双端队列中就绪的epoll节点

3.2 非阻塞io

一般io多路复用搭配非阻塞io使用,后续的系统调用只需要实行操作功能

3.3 reactor 为什么不能搭配非阻塞io

1.多线程环境 —— 惊群
将一个listen fd放到多个epoll去处理,如果使用阻塞io:当一个客户端发起连接,多个线程的epoll被触发,但是只有一个线程的accept会顺利从全连接队列中取得客户端信息,然后连接,其他线程的accept会被阻塞,无法继续进行。(惊群)(TODO : 需要在accept前加锁?)
2.边沿触发下
在边沿触发下,通过会使read在一次事件循环中,把内核中的read buf读空。也就是说,不能等下次边沿触发再去读本次应该读的数据。这就需要read操作的io是非阻塞io。(如果内核的read buf已经为空了,还调用read,则可能会阻塞
3.select bug
image

3.4 是不是io多路复用一定要搭配非阻塞io

3.5 源码


#ifndef _MARK_REACTOR_
#define _MARK_REACTOR_

#include <stdio.h>
#include <unistd.h> // read write
#include <fcntl.h> // fcntl
#include <sys/types.h> // listen
#include <sys/socket.h> // socket
#include <errno.h> // errno
#include <arpa/inet.h> // inet_addr htons
// #include <netinet/tcp.h>
#include <assert.h> // assert

#include <sys/epoll.h>

#include <stdlib.h> // malloc

#include <string.h> // memcpy memmove

#include "chainbuffer/buffer.h"
// #include "ringbuffer/buffer.h"

#define MAX_EVENT_NUM 512
#define MAX_CONN ((1<<16)-1)

typedef struct event_s event_t;
typedef void (*event_callback_fn)(int fd, int events, void *privdata);
typedef void (*error_callback_fn)(int fd, char * err);

typedef struct {
    int epfd;
    int listenfd;
    int stop;
    event_t *events;
    int iter;
    struct epoll_event fire[MAX_EVENT_NUM];
} reactor_t;

struct event_s {
    int fd;
    reactor_t *r;
    buffer_t in;
    buffer_t out;
    event_callback_fn read_fn;
    event_callback_fn write_fn;
    error_callback_fn error_fn;
};

int event_buffer_read(event_t *e);
int event_buffer_write(event_t *e, void * buf, int sz);

reactor_t * create_reactor() {
    reactor_t *r = (reactor_t *)malloc(sizeof(*r));
    r->epfd = epoll_create(1);
    r->listenfd = 0;
    r->stop = 0;
    r->iter = 0;
    r->events = (event_t*)malloc(sizeof(event_t)*MAX_CONN);
    memset(r->events, 0, sizeof(event_t)*MAX_CONN);
    memset(r->fire, 0, sizeof(struct epoll_event) * MAX_EVENT_NUM);
    // init_timer();
    return r;
}

void release_reactor(reactor_t * r) {
    free(r->events);
    close(r->epfd);
    free(r);
}

event_t * _get_event_t(reactor_t *r) {
    r->iter ++;
    while (r->events[r->iter & MAX_CONN].fd > 0) {
        r->iter++;
    }
    return &r->events[r->iter];
}

event_t * new_event(reactor_t *R, int fd,
    event_callback_fn rd,
    event_callback_fn wt,
    error_callback_fn err) {
    assert(rd != 0 || wt != 0 || err != 0);
    event_t *e = _get_event_t(R);
    e->r = R;
    e->fd = fd;
    buffer_init(&e->in, 1024*16);
    buffer_init(&e->out, 1024*16);
    e->read_fn = rd;
    e->write_fn = wt;
    e->error_fn = err;
    return e;
}

void free_event(event_t *e) {
	buffer_free(&e->in);
	buffer_free(&e->out);
}

int set_nonblock(int fd) {
	int flag = fcntl(fd, F_GETFL, 0);
	return fcntl(fd, F_SETFL, flag | O_NONBLOCK);
}

int add_event(reactor_t *R, int events, event_t *e) {
    struct epoll_event ev;
	ev.events = events;
	ev.data.ptr = e;
	if (epoll_ctl(R->epfd, EPOLL_CTL_ADD, e->fd, &ev) == -1) {
        printf("add event err fd = %d\n", e->fd);
		return 1;
	}
	return 0;
}

int del_event(reactor_t *R, event_t *e) {
	epoll_ctl(R->epfd, EPOLL_CTL_DEL, e->fd, NULL);
    free_event(e);
    return 0;
}

int enable_event(reactor_t *R, event_t *e, int readable, int writeable) {
	struct epoll_event ev;
	ev.events = (readable ? EPOLLIN : 0) | (writeable ? EPOLLOUT : 0);
	ev.data.ptr = e;
	if (epoll_ctl(R->epfd, EPOLL_CTL_MOD, e->fd, &ev) == -1) {
		return 1;
	}
	return 0;
}

void eventloop_once(reactor_t * r, int timeout) {
    int n = epoll_wait(r->epfd, r->fire, MAX_EVENT_NUM, timeout);
    for (int i = 0; i < n; i++) {
        struct epoll_event *e = &r->fire[i];
        int mask = e->events;
        if (e->events & EPOLLERR) mask |= EPOLLIN | EPOLLOUT;
        if (e->events & EPOLLHUP) mask |= EPOLLIN | EPOLLOUT;
        event_t *et = (event_t*) e->data.ptr;
        if (mask & EPOLLIN) {
            if (et->read_fn)
                et->read_fn(et->fd, EPOLLIN, et);
        }
        if (mask & EPOLLOUT) {
            if (et->write_fn)
                et->write_fn(et->fd, EPOLLOUT, et);
            else {
                uint8_t * buf = buffer_write_atmost(&et->out);
                event_buffer_write(et, buf, buffer_len(&et->out));
            }
        }
    }
}

void stop_eventloop(reactor_t * r) {
    r->stop = 1;
}

void eventloop(reactor_t * r) {
    while (!r->stop) {
        // int timeout = find_nearest_expire_timer();
        eventloop_once(r, /*timeout*/ -1);
        // expire_timer();
    }
}

int create_server(reactor_t *R, short port, event_callback_fn func) {
	int listenfd = socket(AF_INET, SOCK_STREAM, 0);
	if (listenfd < 0) {
        printf("create listenfd error!\n");
		return -1;
	}
	struct sockaddr_in addr;
	memset(&addr, 0, sizeof(struct sockaddr_in));
	addr.sin_family = AF_INET;
	addr.sin_port = htons(port);
	addr.sin_addr.s_addr = INADDR_ANY;

    int reuse = 1;
	if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (void *)&reuse, sizeof(int)) == -1) {
        printf("reuse address error: %s\n", strerror(errno));
        return -1;
    }

	if (bind(listenfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) < 0) {
        printf("bind error %s\n", strerror(errno));
		return -1;
	}

	if (listen(listenfd, 5) < 0) {
        printf("listen error %s\n", strerror(errno));
		return -1;
	}

    if (set_nonblock(listenfd) < 0) {
        printf("set_nonblock error %s\n", strerror(errno));
        return -1;
    }

    R->listenfd = listenfd;

    event_t *e = new_event(R, listenfd, func, 0, 0);
    add_event(R, EPOLLIN, e);

	printf("listen port : %d\n", port);
	return 0;
}

int event_buffer_read(event_t *e) {
    int fd = e->fd;
    int num = 0;
    while (1) {
        // TODO: dont use char buf[] here
        char buf[1024] = {0};
        int n = read(fd, buf, 1024);
        if (n == 0) {
            printf("close connection fd = %d\n", fd);
            if (e->error_fn)
                e->error_fn(fd, "close socket");
            del_event(e->r, e);
            close(fd);
            return 0;
        } else if (n < 0) {
            if (errno == EINTR)
                continue;
            if (errno == EWOULDBLOCK)
                break;
            printf("read error fd = %d err = %s\n", fd, strerror(errno));
            if (e->error_fn)
                e->error_fn(fd, strerror(errno));
            del_event(e->r, e);
            close(fd);
            return 0;
        } else {
            printf("recv data from client:%s", buf);
            buffer_add(&e->in, buf, n);
        }
        num += n;
    }
    return num;
}

int _write_socket(event_t *e, void * buf, int sz) {
    int fd = e->fd;
    while (1) {
        int n = write(fd, buf, sz);
        if (n < 0) {
            if (errno == EINTR)
                continue;
            if (errno == EWOULDBLOCK)
                break;
            if (e->error_fn)
                e->error_fn(fd, strerror(errno));
            del_event(e->r, e);
            close(e->fd);
        }
        return n;
    }
    return 0;
}

int event_buffer_write(event_t *e, void * buf, int sz) {
    buffer_t *r = &e->out;
    if (buffer_len(r) == 0) {
        int n = _write_socket(e, buf, sz);
        if (n == 0 || n < sz) {
            // 发送失败,除了将没有发送出去的数据写入缓冲区,还要注册写事件
            buffer_add(&e->out, (char *)buf+n, sz-n);
            enable_event(e->r, e, 1, 1);
            return 0;
        } else if (n < 0) 
            return 0;
        return 1;
    }
    buffer_add(&e->out, (char *)buf, sz);
    return 1;
}

#endif

网络编程所用API以及相关数据结构

tcb数据结构

  • Socket包含两部分,一个是IP地址,一个是端口号。同一个设备可以对应一个IP地址,但不同的管道用不同的端口号区分,于是同一个设备发送给其他不同设备的信息就不会产生混乱。在同一时刻,设备可能会产生多种数据需要分发给不同的设备,为了确保数据能够正确分发,TCP协议用一种叫做TCB(Transmission Control Block,传输控制块)的数据结构把发给不同设备的数据封装起来。

  • 一个TCB数据块包含了数据发送双方对应的socket信息以及拥有装载数据的缓冲区。在两个设备要建立连接发送数据之前,双方都必须要做一些准备工作,分配内存建立起TCB数据块就是连接建立前必须要做的准备工作。我们还需要了解的一点是TCP连接的建立方式,由于TCP协议建立在服务器–客户端的模式之上,因此对于两种不同角色的设备,他们发起连接的方式不一样。

  • 客户端发起连接的方式叫主动打开(Active Open)。也就是客户端需要主动向服务器发送消息,表达自己想建立数据连接的请求。服务器发起连接的方式叫被动打开(Passive Open),通常服务器在没有客户端主动请求的时候不知道当前有哪个设备想向它发起连接,因此它只能构建一个端口并监听该端口,等待客户端从该端口向它发起连接请求。在OPEN(打开)阶段无论是客户端还是服务器都需要准备好TCB数据结构,但由于服务器不知道要连接它的客户端的地址信息,因此在构建TCB模块时会默认将客户端对应的Socket数据初始化为0。

  • 当客户端和服务器端双方都把自己的Socket和TCB数据结构准备好后,双方就可以进入所谓的“三次握手”TCP连接建立过程。

tcb对应的结构体

struct tcb {
	short	tcb_state;	/* TCP state				*/
	short	tcb_ostate;	/* output state				*/
	short	tcb_type;	/* TCP type (SERVER, CLIENT)		*/
	int	tcb_mutex;	/* tcb mutual exclusion			*/
	short	tcb_code;	/* TCP code for next packet		*/
	short	tcb_flags;	/* various TCB state flags		*/
	short	tcb_error;	/* return error for user side		*/
	
	IPaddr	tcb_rip;	/* remote IP address			*/
	u_short	tcb_rport;	/* remote TCP port			*/
	IPaddr	tcb_lip;	/* local IP address			*/
	u_short	tcb_lport;	/* local TCP port			*/
	struct	netif	*tcb_pni; /* pointer to our interface		*/

	tcpseq	tcb_suna;	/* send unacked				*/
	tcpseq	tcb_snext;	/* send next				*/
	tcpseq	tcb_slast;	/* sequence of FIN, if TCBF_SNDFIN	*/
	u_long	tcb_swindow;	/* send window size (octets)		*/
	tcpseq	tcb_lwseq;	/* sequence of last window update	*/
	tcpseq	tcb_lwack;	/* ack seq of last window update	*/
	u_int	tcb_cwnd;	/* congestion window size (octets)	*/
	u_int	tcb_ssthresh;	/* slow start threshold (octets)	*/
	u_int	tcb_smss;	/* send max segment size (octets)	*/
	tcpseq	tcb_iss;	/* initial send sequence		*/
	int	tcb_srt;	/* smoothed Round Trip Time		*/
	int	tcb_rtde;	/* Round Trip deviation estimator	*/
	int	tcb_persist;	/* persist timeout value		*/
	int	tcb_keep;	/* keepalive timeout value		*/
	int	tcb_rexmt;	/* retransmit timeout value		*/
	int	tcb_rexmtcount;	/* number of rexmts sent		*/

	tcpseq	tcb_rnext;	/* receive next				*/
	tcpseq	tcb_rupseq;	/* receive urgent pointer		*/
	tcpseq	tcb_supseq;	/* send urgent pointer			*/

	int	tcb_lqsize;	/* listen queue size (SERVERs)		*/
	int	tcb_listenq;	/* listen queue port (SERVERs)		*/
	struct tcb *tcb_pptcb;	/* pointer to parent TCB (for ACCEPT)	*/
	int	tcb_ocsem;	/* open/close semaphore 		*/
	int	tcb_dvnum;	/* TCP slave pseudo device number	*/

	int	tcb_ssema;	/* send semaphore			*/
	u_char	*tcb_sndbuf;	/* send buffer				*/
	u_int	tcb_sbstart;	/* start of valid data			*/
	u_int	tcb_sbcount;	/* data character count			*/
	u_int	tcb_sbsize;	/* send buffer size (bytes)		*/

	int	tcb_rsema;	/* receive semaphore			*/
	u_char	*tcb_rcvbuf;	/* receive buffer (circular)		*/
	u_int	tcb_rbstart;	/* start of valid data			*/
	u_int	tcb_rbcount;	/* data character count			*/
	u_int	tcb_rbsize;	/* receive buffer size (bytes)		*/
	u_int	tcb_rmss;	/* receive max segment size		*/
	tcpseq	tcb_cwin;	/* seq of currently advertised window	*/
	int	tcb_rsegq;	/* segment fragment queue		*/
	tcpseq	tcb_finseq;	/* FIN sequence number, or 0		*/
	tcpseq	tcb_pushseq;	/* PUSH sequence number, or 0		*/
	};

image

服务端

  • socket(): 创建listen_fd
  • bind(): 绑定服务端ip和端口,使listen_fd与ip和端口建立关系,此时连接4元组(src ip, src port, des ip, des port)中的src ip和src port已经准备好,并且tcb准备已经初始化
  • listen(): 监听listen_fd,此时服务端的状态由CLOSE转为LISTEN, 当客户端发送SYN包准备建立连接,服务端会init/add new tcb作为一个新的节点push in syn队列,此时服务端的状态由LISTEN转为SYN_RCVD, 并且服务端会发送SYN + ACK给客户端
  • accept(): 当收到客户端的ACK后,此时会把syn队列中的tcb节点pop出来,再push in accept队列中,服务端状态由SYN_RCVD转为ESTABLISHED,三次握手完成。后续数据传输会从accept队列中取出tcb节点进行数据传输。并且accept为每个节点分配一个socket(conn_fd)
  • recv/read(): 接收数据
  • send/write(): 发送数据
  • close(): 关闭连接

客户端

  • socket(): 创建client_fd
  • bind(): 可选,非必须
  • connect(): 基于client_fd,利用连接4元组向服务端发SYN包,三次握手开始,连接开始建立,此时客户端状态从CLOSE转为SYN_SEND, 注意,connect其实是阻塞的,客户端的状态迁移会阻塞到三次握手成功完成或者失败才会返回,因此客户端此时还要等待服务端回SYN + ACK,并且客户端再回最后1次ACK给服务端,此时客户端状态从SYN_SEND转为ESTABLISHED,三次握手完成。
  • recv/read(): 接收数据
  • send/write(): 发送数据
  • close(): 关闭连接

建立连接阶段

TCP头数据格式

image

序列号的作用:解决数据传输乱序

image

listen时backlog的意义

  • int listen (int socketfd, int backlog)
参数一 socketfd 为 socketfd 文件描述符
参数二 backlog,参数历史版本有所变化
早期backlog 是 SYN队列大小,也就是未完成连接的队列大小
后来backlog 变成 accept 队列,也就是已完成连接的队列长度, 所以现在通常认为backlog 是accept队列
但是上限值是内核参数 somaxconn 的⼤⼩,也就说 accpet 队列⻓度 = min(backlog, somaxconn)。
  • linux内核中会维护两个队列:syn队列与accept队列

半连接队列 —— syn队列

  • 接收到一个syn建立连接请求,服务端处于syn_rcvd状态,此时未完成连接

全连接队列 —— accept队列

  • 已完成tcp3次握手过程,处于established状态,此时已完成连接

服务端三次握手syn队列与accept队列示意图

image

listen_fd 能不能发送数据?

  • 可以,三次握手时,服务端收到客户端第一次SYN后,给其回SYN + ACK时,就是用listen_fd回的
    image

syn泛洪攻击(待学习)

https://blog.csdn.net/bandaoyu/article/details/109400717

数据传输阶段

image

send 发送与tcp粘包、分包

  • 客户端发送数据时,可能会出现1次send或多次send的情况,比如发送几个G的视频,服务端传输层如果用tcp,则需要考虑分包、粘包的处理,因为tcp时面向字节流,只能对发送的字节流进行保序,但是服务端并不知道1次应该读多少数据,因为应用层对传输层进行封装协议的时候,需要对数据做分包处理,常见的有两种措施:
  1. 应用层协议头前面加包长度:
    每次解析到tcp的playload时,取前1个word(提前约定好,也可以是2个字节等其他长度)作为包长度,然后对字节流直接偏移或循环读包长度的码流
  2. 为每个包加分隔符:
    比如http协议每个包直接以\r\n\r\n作为包 end 的标志

服务端不能通过send的返回值 > 0 来判断发送是否成功

  • send的作用仅是把data拷贝到协议栈中send buffer中,后续内核驱动网卡将buffer中的数据发送出去,然后利用连接4元组中的dest ip在路由中不断找下一跳去发包,直到对端的网卡中,当send > 0仅代表数据拷贝到协议栈中成功,对方是否接收数据成功发送端是判断不了的,如果发送端需要知道接收端是否接收数据成功,需要接收端给发送端返回一个值来确定(应用层面确定数据发送成功)系统底层确定数据是否发送成功(协议层面确定数据发送成功)

服务器探测机制——心跳包

  • 定时机制,检测客户端是否在线,宕机
  • 服务器给客户端发数据发现失败

惊群

断开连接阶段

半关闭与shutdown

参考
image
image
image

  • 补充说明:参考文章是多年前的多进程技术,现在基于IO多路复用其实不会由于fork产生子进程使得socket_fd的引用计数 != 1而产生这种情况(但其他情况会导致)

4次挥手状态

image

大量的close_wait状态的原因?怎么解决

https://www.cnblogs.com/grey-wolf/p/10936657.html

  • 服务端迟迟不回FIN,其实可以用异步IO将应用层与传输层解耦

大量的time_wait状态的原因?怎么解决

https://blog.csdn.net/weixin_44844089/article/details/115626152

tcp状态转换记录

用一个基础的tcp server来记录tcp的状态转换,客户端用nc模拟
image

1.server开启监听,client用nc模拟请求建立

实验条件:server的fd不做特殊处理,仅监听9999端口

  • 服务端开启
    image
  • 客户端请求建立连接
    image
  • netstat查看tcp状态
    发现客户端和服务端都处于ESTABLISHED状态
    image

2.服务端通过ctrl+c(发送SIGINT信号)使服务端强制终止

  • 首先,服务端ctrl+c关闭,先发送FIN给客户端,服务端先进入FIN_WAIT1状态,此时客户端回了ACK,服务端很快切到FIN_WAIT2状态,等待客户端发FIN,客户端也切到了CLOSE_WAIT状态
    image
  • 接着,客户端ctrl+c关闭,发送了FIN给服务端,客户端进入LAST_ACK状态,此时服务端(其实是内核)发送了ACK给客户端,服务端切到了TIME_WAIT状态,注意此时服务端等待2MSL再切到CLOSE状态,客户端收到ACK自然的切到了CLOSE状态
    image
  • 服务端主动发起关闭
    注意:如果是客户端主动发起则反过来,谁主动发起关闭,谁才有机会进入TIME_WAIT状态
    image

3.SO_REUSEADDR 的作用

下面这段代码是端口复用的意思,如果使用需要在服务端bind之前使用,它有两个作用

int opt = 1;
setsockopt(listenFd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
  1. 针对服务端重启后TIME_WAIT状态不能正常重启成功——用来避免bind报错“Address in use”
    image
  2. 如果 TCP 服务进程 A 绑定的地址是 0.0.0.0 和端口 8888,而如果 TCP 服务进程 B 绑定的地址是 192.168.1.100 地址(或者其他地址)和端口 8888,那么执行 bind() 时候也会出错
    image

参考:

https://www.jianshu.com/p/4138fc17b126
https://xiaolincoding.com/

posted @ 2022-12-18 12:21  胖白白  阅读(86)  评论(0编辑  收藏  举报