网络内核之TCP是如何发送和接收消息
网络内核之TCP是如何发送和接收消息的
老规矩,带着问题阅读:
- 三次握手中服务端做了什么?
- 为什么要将accept()单独一个线程而不是和读写的io线程共用一个线程池?netty分为boss和worker
- 当调用send()返回后数据就一定到对方或者在网线中传输了呢?
我们先来回顾一下,我们编写一个网络程序有哪些步骤? 基于socket的编程:
代码如下:
public class Server { public static void main(String[] args) throws Exception { //创建一个socket套接字,开始监听某个端口 对应了 socket() bind() listen() ServerSocket serverSocket = new ServerSocket(8080); // (1) 接收新连接线程 new Thread(() -> { while (true) { try { // 等待客户端连接,accept() 获取一个新连接 Socket socket = serverSocket.accept(); new Thread(() -> { try { byte[] data = new byte[1024]; InputStream inputStream = socket.getInputStream(); while (true) { int len; // 读取字节数组 对应read() while ((len = inputStream.read(data)) != -1) { System.out.println(new String(data, 0, len)); } } } catch (IOException e) { } }).start(); } catch (IOException e) {} } }).start(); } } public class Client { public static void main(String[] args) { try { //对应 socket() 和 connect() 发起连接 Socket socket = new Socket("127.0.0.1", 8000); while (true) { try { //对应 write() 方法 socket.getOutputStream().write((new Date() + ": hello world").getBytes()); socket.getOutputStream().flush(); Thread.sleep(2000); } catch (Exception e) { } } } catch (IOException e) { } } }
服务端我们首先会创建一个监听套接字,然后给这个套接字绑定一个ip和端口,这一步对应的方法就是bind(),之后就是调用listen()来监听端口,端口是和应用程序对应的,网卡收到一个数据包的时候后需要知道这个包是给哪个程序用的,当然一个应用程序可以监听多个端口。之后客户端发起连接内核会分配一个随机端口,然后tcp在经历三次握手成功后,客户端会创建一个套接字由connect()方法返回,而服务端的accept()方法也会返回一个套接字,之后双方都会基于这个套接字进行读写操作。所以服务端会维护两种类型的套接字,一种用于监听,另一种用于和客户端进行读写。
而在linux内核中,socket其实是一个文件,挂载于SocketFS文件类型下,有点类似于/proc,不过该文件不能像磁盘上的文件一样进行正常的访问和读写。既然是文件,就会有inode来表示索引,有具体的地方存储数据不管是磁盘还是内存,而socket的数据是存储在内存中的,每个报文的数据是存放在一个叫 sk_buff 的结构体里,要访问文件我们一般会对应一个文件描述符,每个文件描述符都会有一个id,在jdk中也有相关定义。
public final class FileDescriptor { private int fd;
jvm启动后就是一个独立进程,每个进程会维护一个数组,这个数组存放该进程已经打开的文件的描述符,数组前三个分别是标准输入,标准输出,错误输出三个文件描述符,从第4个开始为用户打开的文件,或者创建的socket,而数组的下标就是文件描述符的id,内核通过文件描述符可以找到对应的inode,然后在通过vfs找到对应的文件,进行read和write操作。
三次握手
linux内核中会维护两个队列,这两个队列的长度都是有限制且可以配置的,当客户端发起connect()请求后,服务端收到syn包后将该信息放入sync队列,之后客户端回复ack后从sync队列取出,放到accept队列,之后服务端调用accept()方法会从accept队列取出生成socket。
如果客户端发起sync请求,但是不回复ack,将导致sync队列满载,之后会拒接新的连接。如果客户端发起ack请求后,服务端一直不调用,或者调用accept队列太慢,将导致accept队列满载,accept队列满了则收到ack后无法从syn队列移出去,导致syn队列也会堆积,最终拒绝连接。所以服务端一般会将accept单独起一个线程执行,避免accept太慢导致数据丢弃。当然accept()方法也有阻塞和非阻塞两种,当accept队列为空的时候阻塞方法会一直等待,非阻塞方法会直接返回一个错误码。
消息发送
连接建立好后,客户端和服务端都有一个socket套接字,双方都可以通过各自的套接字进行发送和接收消息,socket里面维护了两个队列,一个发送队列,一个接收队列。
发送的时候数据在用户空间的内存中,当调用send()或者write()方法的时候,会将待发送的数据按照MSS进行拆分,然后将拆分好的数据包拷贝到内核空间的发送队列,这个队列里面存放的是所有已经发送的数据包,对应的数据结构就是sk_buff,每一个数据包也就是sk_buff都有一个序号,以及一个状态,只有当服务端返回ack的时候,才会把状态改为发送成功,并且会将这个ack报文的序号之前的报文都确认掉,如果长期没有确认,会重新调用tcp_push继续发送,如果发送队列慢了,则从用户空间拷贝到内核空间的操作就会阻塞,并触发清理队列中已确认发送成功的数据包。tcp层会将数据包加上ip头然后发给ip层处理,ip层将数据包加入到一个qdisc队列,网卡驱动程序检测到qdisc队列有数据就会调用DMA Engine将sk_buff拷贝到网卡并发送出去,网卡驱动通过ringbuffer来指向内核中的数据,所以qdisc的长度也会影响到网络发送的吞吐量。
关于mss分片:mtu是数据链路层的最大传输单元,一般为1500字节,而一个ip包的最大长度为65535,所以ip层在发送数据前会根据mtu分片,这样一个tcp包本来对应一个ip包,分片后将对应多个ip包,每个包都有一个ip头,在接收端需要等到所有的ip包到达后,才能确定这个tcp收到然后才发送ack,这种方式无疑是低效的,所以tcp层会尽量阻止ip层进行分片,他会在从用户空间拷贝的时候就会按照mtu进行拆分,将一个数据包拆分成多个数据包。但是链路中mtu是会改变的,为了完全避免ip层进行分片,可以在ip层设置一个df标记,如果一定要分片就慧慧一个icmp报文。
关于流控:
- 滑动窗口:接收方返回的一个最大发送序号。这个不是报文大小,而是一个序号,接收方每次会返回一个下次报文发送的序号不要超过的值。这个值主要和接收方内部缓存大小有关。
- 阻塞窗口:发送方根据网络拥堵情况,根据已经发送到网络但是还未确认的数据包的数量来计算。由于广域网络的复杂所以拥塞控制有一系列算法,如慢启动等。
- nagle算法:为了避免机器发了大量的小数据包,nagle算法限制每次将多个小数据包达到一定大小后在发送。
由于tcp发送的时候会进行各种分片和合并,所以接收方会出现粘包现象,需要应用层进行处理。
消息接收
当服务端网卡收到一个报文后,网卡驱动调用DMA engine将数据包通过ringbuffer拷贝到内核缓冲区中,拷贝成功后,发起中断通知中断处理程序,这时候ip层会处理该数据包,之后交给tcp层,最终到达tcp层的recv buffer(接收队列),这时候就会返回ack给客户端,并没有等到客户端调用read将数据从内核拷贝到用户空间,所以应用层也应该有相关的确认机制。如果recv buffer设置的太小,或者应用层一直不来取,那么也将阻塞数据接收,从而影响到滑动窗口大小,导致吞吐量降低。
tcp在收到数据包后会获取序号,并且看是否应该正好放入接收队列,如果此时收到一个大序号的报文,会将该报文缓存直到接收队列中之前的报文已经插入。
另外如果网卡支持多队列,可以将多个队列绑定到不同的cpu上,这样网卡收到报文后,不同的队列就会通过中断触发不同的cpu,从而可以提高吞吐量。
c10k问题
c10k问题是指怎么支持单机1万的并发请求,我们想到通过select的多路复用模式,用一个单独的线程去扫描需要监听的文件描述符,如果这些文件描述符里面有可读或者可写的就返回(tcp层在收到报文拷贝到内存后会修改这个文件描述符的状态),没有就阻塞,不过这种方式需要对文件描述符进行扫描,效率不高。而epoll方式采用红黑树去管理文件描述符,当文件可读或者可写的时候会通过一个回调函数通知用户进行具体的io操作。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
NEXT社区小课堂 | 第五课:NEO-共识算法dBFT源码解析
NEXT社区 | 小课堂 由于近期NEXT社区加入很多新的小伙伴,有在校大学生,有对区块链感兴趣的传统企业从业者。为了更方便、更系统的让NEXT社区的伙伴们了解NEO的技术知识,因此我们开设了小课堂,每周3节,向大家普及NEO相关的知识要点! 公号:NEONEXT NEXT社区小课堂 | 第五课 NEO-共识机制dBFT源码解析 一、NEO DBFT共识算法 dbft改进自算法pbft算法,pbft算法通过多次网络请求确认,最终获得多数共识。其缺点在于随着随着节点的增加,网络开销呈指数级增长,网络通信膨胀,难以快速达成一致。neo的解决方案是通过投票选取出一定数量的节点作为记账人,由此减少网络消耗又可以兼顾交易速度,这也是dbft中d的由来。 二、代码结构说明 ├── Consensus │ ├── ChangeView.cs //viewchange 消息 │ ├── ConsensusContext.cs //共识上下文 │ ├── ConsensusMessage.cs //共识消息 │ ├── ConsensusMessageType.cs //共识消息类型...
- 下一篇
页面防重提交技术之令牌机制
今天给大家带来的是页面防重提交之令牌机制技术,如有不足之处,敬请指正。那么首先我们必须知道页面防重是什么? 例如:我们在浏览器输入表单信息提交后,会将数据插入到数据库中,此时浏览器会保存上一次请求的数据,如果我们不做页面防重,当我们点击页面刷新按钮时,就会再次将已经填好的表单提交,再次插入到数据库中。 浏览器中的防重提交示例 要解决这个问题,首先要理解token机制(令牌机制) 如何理解token机制:通俗的讲就好比古代将军带兵出征打仗,但是将军如果在千里之外需要执行皇帝的命令,只能皇帝派亲信带着皇帝的信物虎符(虎符一分为二)来传达命令。若亲信带着的虎符与将军的虎符匹配,则证明为皇帝真实命令。我们这次的令牌机制与此情景十分相似。 token示意图 Token机制的实现: 我们在进入提交数据的表单页面之前,先创建一个Token给页面 在页面表单设置Token,作为表单的参数 提交数据到服务器后,首先判断是否是一个防重提的表单,如果是,就判断是否是 需要创建Token还是校验删除Token,还是处理重提。 如果是处理重提交表单,需要页面指定一个跳转路径 一、创建一个注解...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS7安装Docker,走上虚拟化容器引擎之路
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- SpringBoot2全家桶,快速入门学习开发网站教程
- Eclipse初始化配置,告别卡顿、闪退、编译时间过长
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- MySQL8.0.19开启GTID主从同步CentOS8
- CentOS7,CentOS8安装Elasticsearch6.8.6
- 2048小游戏-低调大师作品
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7