封面图片

编程

网络编程指南:Socket、TCP和HTTP

很多情况下,我们都会接触到网络模型,Socket、TCP等网络编程概念。但这些技术概念在我们平时的开发中却不是经常碰到。但是为什么它们会频繁的在博客中,面试中出现?主要原因还是我们使用的技术都已经对这些技术完成了封装,我们只需要使用就行,但使用过程中如果出现问题,我们却如无头苍蝇一样,不知道如何下手。这个时候就需要我们去理解技术的原理。


网络模型分层

https://ppsummer.com/blog/preview/552A5A38Pastedimage20230818100037.png

网络分层与基于sock实现网络传输功能

应用层

我们能直接接触到的就是应用层Application Layer),我们电脑或手机使用的应用软件都是在应用层实现。那么,当两个不同设备的应用需要通信的时候,应用就把应用数据传给下一层,也就是传输层。

所以,应用层只需要专注于为用户提供应用功能,比如 HTTP、FTP、Telnet、DNS、SMTP等。

应用层是不用去关心数据是如何传输的,就类似于,我们寄快递的时候,只需要把包裹交给快递员,由他负责运输快递,我们不需要关心快递是如何被运输的。

传输层

在传输层会有两个传输协议,分别是 TCP 和 UDP。

TCP 的全称叫传输控制协议(Transmission Control Protocol),大部分应用使用的正是 TCP 传输层协议,比如 HTTP 应用层协议。TCP 相比 UDP 多了很多特性,比如流量控制、超时重传、拥塞控制等,这些都是为了保证数据包能可靠地传输给对方。

UDP 相对来说就很简单,简单到只负责发送数据包,不保证数据包是否能抵达对方,但它实时性相对更好,传输效率也高。当然,UDP 也可以实现可靠传输,把 TCP 的特性在应用层上实现就可以,不过要实现一个商用的可靠 UDP 传输协议,也不是一件简单的事情。

应用需要传输的数据可能会非常大,如果直接传输就不好控制,因此当传输层的数据包大小超过 MSS(TCP 最大报文段长度) ,就要将数据包分块,这样即使中途有一个分块丢失或损坏了,只需要重新发送这一个分块,而不用重新发送整个数据包。在 TCP 协议中,我们把每个分块称为一个 TCP 段TCP Segment)。

网络层

网络层的协议就是IP协议,负责将数据从一端传递到另一端。

IP 协议是TCP/IP 协议族中最为核心的协议,更确切的说是网络层重要的协议之一。 IP 协议把上层数据包封装成IP 数据包后进行传输,如果IP 数据包太大,还要对数据包进行分片后再传输,到了目的地址处再进行组装还原,以适应不同物理网络对一次所能传输数据大小的要求。

各层数据包格式

网络接口层的传输单位是帧(frame),IP 层的传输单位是包(packet),TCP 层的传输单位是段(segment),HTTP 的传输单位则是消息或报文(message)。

https://ppsummer.com/blog/preview/DE55457EPastedimage20230814192546.png

数据的层层封装

Socket

https://ppsummer.com/blog/preview/6234B5C3Pastedimage20230818092622.png

socket建立连接

Socket要做的事情就是将一台电脑的进程跟另一台电脑的进程建立连接。

Socket是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口 (API),通过Socket,我们才能使用TCP/IP协议。

Socket可以支持不同的传输层协议(TCP或UDP),当使用TCP协议进行连接时,该Socket连接就是一个TCP连接。

是操作系统内核里的一部分,用来帮助进程调用硬件资源,比如网卡。

sock

sock是socket用来收发数据的一个数据结构。里面包含了IP和端口。

为了实现不同类型的网络传输功能,通过继承sock就实现了应用在不同场景下的sock。

https://ppsummer.com/blog/preview/01514398Pastedimage20230818093247.png

sock继承

inet_sock特指用了网络传输功能的sock,在sock的基础上还加入了TTL,端口,IP地址这些跟网络传输相关的字段信息。Unix domain socket,用于本机进程之间的通信,直接读写文件,不需要经过网络协议栈。这是个非常有用的东西。

inet_connection_sock 是指面向连接的sock,在inet_sock的基础上加入面向连接的协议里相关字段,比如accept队列,数据包分片大小,握手失败重试次数等。

tcp_sock 就是正儿八经的tcp协议专用的sock结构了,在inet_connection_sock基础上还加入了tcp特有的滑动窗口、拥塞避免等功能。同样udp协议也会有一个专用的数据结构,叫udp_sock。

为什么高并发系统需要修改Linux的文件最大打开数?

Linux 是文件即系统,最大文件打开数影响着请求连接数量,一般情况下是1024。这个是一般的解释。

更近一步来说就是,建立连接需要创建一个sock,在创建这个sock的时候,同时创建一个文件,操作这个文件就是操作这个sock,将文件的文件句柄交给进程,让进程通过这个句柄去控制sock。

https://ppsummer.com/blog/preview/0B7D89FAPastedimage20230818094229.png

通过文件句柄控制sock

HTTP

HTTP协议即超文本传送协议(Hypertext Transfer Protocol ),是Web联网的基础,也是手机联网常用的协议之一,HTTP协议是建立在TCP协议之上的一种应用。

HTTP连接最显著的特点是客户端发送的每次请求都需要服务器返回响应,在请求结束后,会主动释放连接。从建立连接到关闭连接的过程称为“一次连接”。

在HTTP 1.0中,客户端的每次请求都要求建立一次单独的连接,在处理完本次请求后,就自动释放连接。

在HTTP 1.1中则可以在一次连接中处理多个请求,并且多个请求可以重叠进行,不需要等待一个请求结束后再发送下一个请求。

HTTP消息信息是用ASCII编码的,每个HTTP请求消息均包含HTTP协议版本(HTTP/1.1,HTTP/2),HTTP方法(GET/POST等),HTTP标头(Content-Type,Content-Length),主机信息等。以及包含要传输到服务器的实际消息的正文(请求主体)。

有个比较形象的描述:HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通信的能力。

和WebSocket的关系

WebSocket和HTTP一样,都位于网络模型的应用层。

WebSocket是双向的,在客户端-服务器通信的场景中使用的全双工协议,它以ws://或wss://开头。与HTTP不同的是,Websocket需要先建立连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。而HTTP请求可能需要在每个请求都携带状态信息(如身份认证等)。这意味着客户端和服务器之间的连接将保持活动状态,直到被任何一方(客户端或服务器)终止。在通过客户端和服务器中的任何一方关闭连接之后,连接将从两端终止。

连接状态

linux下使用netstat命令查看端口连接状态。

  1. 通过postman访问端口为12800的接口时,状态是ESTABLISHED。
1> netstat -nat | grep -i 12800 2tcp6 0 0 :::12800 :::* LISTEN 3tcp6 0 0 192.168.150.233:12800 192.168.14.8:57350 ESTABLISHED 4

当短时间内多次调用会复用该连接。

  1. 当大概一分钟之后再次进行调用会发现会建立新的连接,原来的链接变为TIME_WAIT
1> netstat -nat | grep -i 12800 2tcp6 0 0 :::12800 :::* LISTEN 3tcp6 0 0 192.168.150.233:12800 192.168.14.8:57662 ESTABLISHED 4tcp6 0 0 192.168.150.233:12800 192.168.14.8:57350 TIME_WAIT

同时,我们发现在通过postman进行接口调用时,访问端的端口是socket在建立连接时通过算法生成的。客户端使用的端口号范围是:49152~65535。 公认端口(Well Known Ports):0-1023之间的端口号。 常用的公认端口有:
FTP : 21
TELNET : 23
SMTP : 25
DNS : 53
TFTP : 69
HTTP : 80
HTTPS: 443 SNMP : 161

注册端口(Registered Ports):从1024-49151。

  1. 当通过nginx代理访问12800接口时,通过监听12800端口发现
1> netstat -nat | grep -i 12800 2tcp6 0 0 :::12800 :::* LISTEN 3tcp6 0 0 127.0.0.1:12800 127.0.0.1:60082 TIME_WAIT 4tcp6 0 0 ::1:12800 ::1:36186 TIME_WAIT

调用完状态就直接是TIME_WAIT。同时会有个注册端口36186的地址出现。

当不停地通过页面访问nginx代理的网页,通过nginx代理访问12800接口时。

1> netstat -nat | grep -i 12800 2tcp6 0 0 :::12800 :::* LISTEN 3tcp6 0 0 127.0.0.1:12800 127.0.0.1:60082 TIME_WAIT 4tcp6 0 0 ::1:12800 ::1:36200 TIME_WAIT 5tcp6 0 0 ::1:12800 ::1:36196 TIME_WAIT 6tcp6 0 0 ::1:12800 ::1:36186 TIME_WAIT 7tcp6 0 0 127.0.0.1:12800 127.0.0.1:60092 TIME_WAIT 8tcp6 0 0 ::1:12800 ::1:36204 TIME_WAIT 9tcp6 0 0 127.0.0.1:12800 127.0.0.1:60096 TIME_WAIT

会出现多个链接。表明nginx代理访问后端接口的连接没有复用,这是因为nginx代理后台接口时,默认没有使用长连接。

查看nginx进程的连接状态。

1> netstat -anp | grep nginx 2 3tcp 0 0 0.0.0.0:8888 0.0.0.0:* LISTEN 343982/nginx: maste 4tcp 0 0 0.0.0.0:11800 0.0.0.0:* LISTEN 343982/nginx: maste 5tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 343982/nginx: maste 6tcp 0 0 0.0.0.0:8083 0.0.0.0:* LISTEN 343982/nginx: maste 7tcp 0 0 192.168.150.233:80 192.168.14.8:55378 ESTABLISHED 343983/nginx: worke 8tcp 0 0 192.168.150.233:80 192.168.14.8:55230 ESTABLISHED 343983/nginx: worke 9tcp 0 0 192.168.150.233:80 192.168.14.8:55200 ESTABLISHED 343983/nginx: worke

发现nginx跟客户端的连接是一个ESTABLISHED的状态。连接是可以复用的。 默认情况下,nginx已经自动开启了对客户端的连接的keep alive支持。

1http{ 2 ## 长连接保持时间 3 keepalive_timeout 65; 4 ## 一个长连接的最大请求数 5 keepalive_requests 100; 6}

nginx代理后端地址需要配置长连接。 为了让nginx和server(nginx称为upstream)之间保持长连接,典型设置如下:

http { upstream BACKEND { ... keepalive: 300; //很重要 } server { location / { ... proxy_http_version 1.1; //这2个最好设置以下 proxy_set_header Connection ""; } } }

第一个参数是keepalive,这个的含义是指空闲连接数。 根据QPS和平均响应时间大体能计算出需要的长连接的数量。比如前面10000 QPS和100毫秒响应时间就可以推算出需要的长连接数量大概是1000. 然后将keepalive设置为这个长连接数量的10%到30%。或者直接设置为1000。

proxy_http_version和proxy_set_header最好也设置一下。

通过以上配置之后,再次测试nginx代理后台接口的状态情况。

1> netstat -anp | grep 12800 2tcp 0 0 127.0.0.1:33692 127.0.0.1:12800 ESTABLISHED 2187147/nginx: work 3tcp6 0 0 :::12800 :::* LISTEN 1873072/java 4tcp6 0 0 127.0.0.1:12800 127.0.0.1:33692 ESTABLISHED 1873072/java 5tcp6 0 0 ::1:12800 ::1:38028 ESTABLISHED 1873072/java 6tcp6 0 0 ::1:38028 ::1:12800 ESTABLISHED 2187147/nginx: work

发现连接状态是ESTABLISHED。通过不停的访问前端页面,页面发送请求到后台接口,都没有新的连接的建立,是复用的原有连接。

通过查看nginx的连接情况可以看出来。

1> netstat -anp | grep nginx 2tcp 0 0 0.0.0.0:8888 0.0.0.0:* LISTEN 2187146/nginx: mast 3tcp 0 0 0.0.0.0:11800 0.0.0.0:* LISTEN 2187146/nginx: mast 4tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 2187146/nginx: mast 5tcp 0 0 0.0.0.0:8083 0.0.0.0:* LISTEN 2187146/nginx: mast 6tcp 0 0 127.0.0.1:33934 127.0.0.1:12800 ESTABLISHED 2187147/nginx: work 7tcp 0 0 192.168.150.233:80 192.168.14.8:55108 ESTABLISHED 2187147/nginx: work

最后2行的意思是:192.168.14.8 使用端口55108跟192.168.150.233的80端口建立了长连接,访问页面。 nginx使用生成的端口33934访问了本机的12800端口,这个端口是后台接口地址,也是长连接。

2023年08月17日
在初学者眼中,世界充满了可能;专家眼中,世界大都已经既定。--铃木俊隆