Linux高性能网络编程:TCP底层的收发过程

今天探索高性能网络编程,但是我觉得在谈系统API之前可以先讲一些Linux底层的收发包过程,如下这是一个简单的socket编程代码:

int main() {
    ... 

    fd = socket(AF_INET, SOCKET_STREAM, 0);
    bind(fd, ...);
    listen(fd, ...);

    // 如何建立连接
    ...
    afd = accept(fd, ...);

    // 如何接收数据
    ...
    read(afd, ...);

    // 如何发送数据
    ...
    send(afd, ...);

    // 如何关闭连接
    ...
    close(fd);
    ...
}

第一部分:如何建立连接

​我们知道TCP/IP协议族划分了应用层、TCP传输层、IP网络层、链路层(以太层驱动)。

如上图看应用层,通常在网络编程中我们需要调用accept的API建立TCP连接,那TCP如何做的呢?

​从上图的流程可以看到:

(1)client端发起TCP握手,发送syn包;

(2)内核收到包以后先将当前连接的信息插入到网络的SYN队列;

(3)插入成功后会返回握手确认(SYN+ACK);

(4)client端如果继续完成TCP握手,回复ACK确认;

(5)内核会将TCP握手完成的包,先将对应的连接信息从SYN队列取出;

(6)将连接信息丢入到ACCEPT队列;

(7)应用层sever通过系统调用accept就能拿到这个连接,整个网络套接字连接完成;

那基于这个图,我想问问读者这里会有什么问题么? 细心的读者应该可以看出:

1、这里有两个队列,必然会有满的情况,那如果遇到这种情况内核是怎么处理的呢?

(1)如果SYN队列满了,内核就会丢弃连接;

(2)如果ACCEPT队列满了,那内核不会继续将SYN队列的连接丢到ACCEPT队列,如果SYN队列足够大,client端后续收发包就会超时;

(3)如果SYN队列满了,就会和(1)一样丢弃连接;

2、如何控制SYN队列和ACCEPT队列的大小?

(1)内核2.2版本之前通过listen的backlog可以设置SYN队列(半连接状态SYN_REVD)和ACCEPT队列(完全连接状态ESTABLISHED)的上限;

(2)内核2.2版本以后backlog只是表示ACCEPT队列上限,SYN队列的上限可以通过/proc/sys/net/ipv4/tcp_max_syn_backlog设置;

3、server端通过accept一直等,岂不是会卡住收包的线程?

在linux网络编程中我们都会追求高性能,accept如果卡住接收线程,性能会上不去,所以socket编程中就会有阻塞和非阻塞模式。

(1)阻塞模式下的accept就会卡住,当前线程什么事情都干不了;

(2)非阻塞模式下,可以通过轮询accept去处理其他的事情,如果返回EAGAIN,就是ACCEPT队列为空,如果返回连接信息,就是可以处理当前连接;

相关视频推荐

tcpip,accept,11个状态,细枝末节的秘密,还有哪些你不知道

dpdk从tcp/ip协议栈开始,准备好linux环境一起开始

10道面试必问的经典网络八股文,让你在面试中逼格满满

免费学习地址:c/c++ linux服务器开发/后台架构师

需要C/C++ Linux服务器架构师学习资料加qun812855908获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享

第二部分:接收数据

​(1)当网卡接收到报文并判断为TCP协议后,将会调用到内核的tcp_v4_rcv方法,如果数据按顺序收到S1数据包,则直接插入receive队列中;

(2)当收到了S3数据包,在第1步结束后,应该收到S2序号,但是报文是乱序进来的,则将S3插入out_of_order队列(这个队列存储乱序报文);

(3)接下来收到S2数据包,如第1步直接进入receive队列,由于此时out_of_order队列不像第1步是空的,所以引发了接来的第4步;

(4)每次向receive队列插入报文时都会检查out_of_order队列,如果遇到期待的序号S3,则从out_of_order队列摘除,写入到receive队列;

(5)现在应用程序开始调用recv方法;

(6)经过层层封装调用,接收TCP消息最终会走到tcp_recvmsg方法;

(7)现在需要拷贝数据从内核态到用户态,如果receive队列为空,会先检查SO_RCVLOWAT这个阀值(0表示收到指定的数据返回,1表示只要读取到数据就返回,系统默认是1),如果已经拷贝的字节数到现在还小于它,那么可能导致进程会休眠,等待拷贝更多的数据;

(8)将数据从内核态拷贝到用户态,recv返回拷贝数据的大小;

(9)为了选择降低网络包延时或者提升吞吐量,系统提供了tcp_low_latency参数,如果为0值,用户暂时没有读数据则数据包进入prequeue队列,提升吞吐量,否则不使用prequeue队列,进入tcp_v4_do_rcv,降低延时;

第三部分:发送数据

​(1)假设调用send方法来发送大于一个MSS(比如2K)的数据;

(2)内核调用tcp_sendmsg,实现复制数据,写入队列和组装tcp协议头;

(3)在调用tcp_sendmsg先需要在内核获取skb,将用户态数据拷贝到内核态,内核真正执行报文的发送,与send方法的调用并不是同步的,即send方法返回成功,也不一定把IP报文都发送到网络中了。因此,需要把用户需要发送的用户态内存中的数据,拷贝到内核态内存中,不依赖于用户态内存,也使得进程可以快速释放发送数据占用的用户态内存。但这个拷贝操作并不是简单的复制,而是把待发送数据,按照MSS来划分成多个尽量达到MSS大小的分片报文段,复制到内核中的sk_buff结构来存放;

(4)将数据拷贝到发送队列中tcp_write_queue;

(5)调用tcp_push发送数据到IP层,这里主要滑动窗口,慢启动,拥塞窗口的控制和判断是否使用Nagle算法合并小报文(上一篇已经有介绍);

(6)组装IP报文头,通过经过iptables或者tcpdump等netfilter模块过滤,将数据交给邻居子系统(主要功能是查找需要发送的MAC地址,发送arp请求,封装MAC头等);

(7)调用网卡驱动程序将数据发送出去;

第四部分:关闭连接

关闭连接就是TCP挥手过程,我们都知道TCP连接是一种可靠的连接,那如何才能完整可靠的完成关闭连接呢?linux系统提供了两个函数:

  • close对应tcp_close方法,通过减少socket的引用次数实现关闭,仅当引用计数为0时才会触发tcp_close;

  • shutdown对应tcp_shutdown方法,不关心socket被引用次数,直接关闭对应的连接;

​(1)shutdown可携带一个参数,取值有3个,分别意味着:只关闭读、只关闭写、同时关闭读写;

(2)若shutdown的是半打开的连接,则发出RST来关闭连接;

(3)若shutdown的是正常连接,那么关闭读其实与对端是没有关系的;

(4)若参数中有标志位为关闭写,那么下面做的事与close是一致的,发出FIN包,告诉对方本机不会再发消息了;

第五部分:思考题

基于本文留几个思考题。

(1)发送方法返回成功后,数据一定发送到了TCP的对端么? (调用了IP层的方法返回后,也未必就保证此时数据一定发送成功)

(2)1个socket套接字可能被多个进程在使用,出现并发访问时,内核是怎么处理这种状况的?

(3)若socket为默认的阻塞套接字,调用recv方法传入的len参数,如果网络包的数据小于len,recv会返回么?

(4)当socket被多进程或者多线程共享时,关闭连接时有何区别?

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Linux多线程服务端编程是指使用Muduo C网络库在Linux操作系统中进行多线程的服务端编程。Muduo C网络库是一个基于事件驱动的网络库,采用了Reactor模式,并且在底层使用了epoll来实现高效的I/O复用。 使用Muduo C网络库进行多线程服务端编程有以下几个步骤: 1. 引入Muduo C网络库:首先需要下载并引入Muduo C网络库的源代码,然后在编写代码时包含相应的头文件。 2. 创建并初始化EventLoop:首先需要创建一个EventLoop对象,它用于接收和分发事件。通过初始化函数进行初始化,并在主线程中调用它的loop()函数来运行事件循环。 3. 创建TcpServer:然后创建一个TcpServer对象,它负责监听客户端的连接,并管理多个TcpConnection对象。通过设置回调函数,可以在特定事件发生时处理相应的逻辑。 4. 创建多个EventLoopThread:为了提高并发性能,可以创建多个EventLoopThread对象,每个对象负责一个EventLoop,从而实现多线程处理客户端的连接和请求。 5. 处理事件:在回调函数中处理特定事件,例如有新的连接到来时会调用onConnection()函数,可以在该函数中进行一些初始化操作。当有数据到来时会调用onMessage()函数,可以在该函数中处理接收和发送数据的逻辑。 6. 运行服务端:在主线程中调用TcpServer的start()函数来运行服务端,等待客户端的连接和请求。 总的来说,使用Muduo C网络库进行Linux多线程服务端编程可以更好地利用多核处理器的性能优势。每个线程负责处理特定事件,通过事件驱动模式实现高效的网络编程。这样可以提高服务器的并发能力,提高系统的整体性能。 ### 回答2: Linux多线程服务端编程是指在Linux平台上使用多线程的方式来编写网络服务器程序。而使用muduo C网络库是一种常见的方法,它提供了高效的网络编程接口,可以简化多线程服务器的开发过程。 muduo C网络库基于Reactor模式,利用多线程实现了高并发的网络通信。在使用muduo C进行多线程服务端编程时,我们可以按照以下步骤进行: 1. 引入muduo库:首先需要导入muduo C网络库的头文件,并链接对应的库文件,以供程序调用。 2. 创建线程池:利用muduo C中的ThreadPool类创建一个线程池,用于管理和调度处理网络请求的多个线程。 3. 创建TcpServer对象:使用muduo C中的TcpServer类创建一个服务器对象,监听指定的端口,并设置好Acceptor、TcpConnectionCallback等相关回调函数。 4. 定义业务逻辑:根据具体的业务需求,编写处理网络请求的业务逻辑代码,如接收客户端的请求、处理请求、发送响应等。 5. 注册业务逻辑函数:将定义好的业务逻辑函数注册到TcpServer对象中,以便在处理网络请求时调用。 6. 启动服务器:调用TcpServer对象的start函数,启动服务器,开始监听端口并接收客户端请求。 7. 处理网络请求:当有客户端连接到服务器时,muduo C会自动分配一个线程去处理该连接,执行注册的业务逻辑函数来处理网络请求。 8. 释放资源:在程序结束时,需要调用相应的函数来释放使用的资源,如关闭服务器、销毁线程池等。 通过使用muduo C网络库,我们可以简化多线程服务端编程的过程,提高服务器的并发处理能力。因为muduo C网络库已经实现了底层网络通信细节,我们只需要专注于编写业务逻辑代码,从而减少开发的工作量。同时,muduo C的多线程模型可以有效地提高服务器的并发性能,满足高并发网络服务的需求。 ### 回答3: Linux多线程服务端编程是指在Linux操作系统上开发多线程的服务器应用程序。使用muduo C网络库有助于简化开发过程,提供高效的网络通信能力。 muduo C网络库是一个基于Reactor模式的网络库,适用于C++语言,由Douglas Schmidt的ACE网络库演化而来。它提供了高度并发的网络编程能力,封装了许多底层细节,使得开发者能够更加专注于业务逻辑的实现。 在开发过程中,首先需要创建一个muduo C的EventLoop对象来管理事件循环。然后,可以利用TcpServer类来创建服务器并监听指定的端口。当有新的客户端请求到达时,muduo C会自动调用用户定义的回调函数处理请求。 在处理请求时,可以使用muduo C提供的ThreadPool来创建多个工作线程。这些工作线程将负责处理具体的业务逻辑。通过将工作任务分配给不同的线程,可以充分利用多核服务器的计算资源,提高服务器的处理能力。 在具体的业务逻辑中,可以使用muduo C提供的Buffer类来处理网络数据。Buffer类提供了高效的数据读写操作,可以方便地进行数据解析与封装。 此外,muduo C还提供了TimerQueue类来处理定时任务,可以用于实现定时事件的调度与管理。这对于一些需要定期执行的任务非常有用,如心跳检测、定时备份等。 总之,使用muduo C网络库可以简化Linux多线程服务端编程的开发过程,提供高效的并发能力。通过合理地利用多线程和其他的相关组件,可以实现高性能、稳定可靠的网络服务端应用程序。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值