6. TCP 三次握手四次挥手
HTTP 协议是 Hype Transfer Protocol(超文本传输协议)的缩写,是用于从万维网(WWW:World Wide Web)服务器(sever)传输超文本到客户端(本地浏览器 client)的传送协议。HTTP 协议基于 TCP/IP 协议之上,HTTPS 基于 TLS/SSL 协议层上,两者都是属于应用层的面向对象的协议。
由上图可知,HTTP 协议工作前需要 client 与 sever 建立连接。该连接由 tcp 来完成,tcp 与 ip 共同组成了 Internet,也就是著名的 TCP/IP 通信协议。
6.1 TCP 简介
TCP(Transmission Control Protocol)全名传输控制协议,是主机对住几层的传输控制协议,提供可靠的连接服务,采用三次握手来建立一个连接。与 UDP 都是传输层的协议,比 UDP 更可靠,默认端口 80 。
TCP标志位(位码):
- SYN(synchronous):建立连接
- ACK(acknowledgement):确认
- ack:确认号
- PSH(push):传送
- FIN(finish):结束
- RST(reset):重置
- URG(urgent):紧急
- Sequence number:顺序号码
- Acknowledge number:确认号码
6.2 三次握手
最初两端 TCP 进程都处于关闭状态,client 主动打开连接,server 被动打开连接。大致步骤:client、server 关闭 —— server 收听到 listen —— client 同步已发送状态 SYN-SENT —— server 同步收到状态 SYN_RCVD —— client、server 已建立状态 ESTABLEISHED。
规定:SYN=1 的报文不传输数据,并消耗一个随机序列号。
1、第一次
client 向 server 发送连接请求报文 SYN=1
,同时生成初始序列化 seq=x
,此时 client 进入 SYN-SENT
(同步已发送)状态。
2、第二次
server 收到请求报文后,如果同意连接,则发出确认报文。确认报文中包含:ACK=1,SYN=1
,确认号:ack=x+1(即上一次的seq+1)
,同时也要为自己随机初始化一个序列号 seq=y
。此时 server 进入 SYN-RCVD
(同步收到)状态。这个报文也没有携带数据,循环 client 是否准备好。
3、第三次
client 收到确认后,向 server 给出确认:ACK=1(与 server 给出的一致),ack=y+1
,此时 client 连接建立,进入 ESTABLISHED
状态。这里客户端表示已经准备好了。
6.3 四次挥手
1、第一次
client 发送一个 FIN ,用来结束连接。client 进程发出连接释放报文,并停止发送数据。释放报文首部:FIN=1
,序列号 seq=i
。
此时 client 进入 FIN_WAIT_1
(终止等待1)状态。
2、第二次
server 收到这个 FIN 后,返回一个 ACK
(确认),确认序号:ack=i+1
。同时携带自己的序列号 seq=j
。
此时, server 进入 CLOSED_WAIT
(关闭等待)状态。
并通知高层的应用进程,此时处于半关闭状态,client 没有数据发送了,但 server 若发送数据,client 依然会接收,这种状态还会持续一段时间。
3、第三次
server 将最后的数据发送完毕后,发送一个 FIN
(结束),确认序号:ack=i+1
,同时携带序号 seq=w
,准备 关闭 client 的连接,等待 client 的最后确认。
此时,server 进入 LAST_ACK
(最后确认)状态。
4、第四次
client 发送 ACK
确认,并将确认序号+1:ack=w+1
,而自己序列号 seq=i+1
。
此时,client 进入 TIME_WAIT
(时间等待)状态。
**Note:**此时 client 并没有释放,必须等待 2MSL(最长报文段寿命)使君子后,当 server 撤销相应 TCB 后,从进入
CLOSED
状态。server 只要收到了 client 发出的确认,立即进入CLOSED
状态。同样,撤销TCB后,就结束了这次的TCP连接
为什么会是四次挥手?
三次握手时没有数据传输,而四次挥手时涉及到有数据传输。client 发出关闭请求,表示已经数据传输完毕,但是 server 有可能数据还未传输完毕,这时就需要已 server 端数据是否传输完毕为标准,因此需要四次。
当高并发时,现实情况往往是 server 先断开 client 连接,因为多保存 client 一次连接,就会多占用一些资源。因此在短时间内再次向 server 发起连接,会提示 serve time_wait。
客户端突然挂掉了怎么办?
正常连接时,客户端突然挂掉了,如果没有措施处理这种情况,那么就会出现客户端和服务器端出现长时期的空闲。解决办法是在服务器端设置保活计时器,每当服务器收到
客户端的消息,就将计时器复位。超时时间通常设置为2小时。若服务器超过2小时没收到客户的信息,他就发送探测报文段。若发送了10个探测报文段,每一个相隔75秒,
还没有响应就认为客户端出了故障,因而终止该连接。
参考文章:https://www.cnblogs.com/qdhxhz/p/8470997.html
7. 客户端服务端循环发送信息
之前设计的 socket 程序只能进行一次发送接收就终止掉了,而现实情况不可能只有一次发送与接收,往往都是循环往复,那么就需要给 socket client 和 socket server 添加循环机制。
服务端:
from socket import *
ip_port = ('127.0.0.1', 8000)
back_log = 5
buffer_size = 1024
tcp_server = socket(AF_INET, SOCK_STREAM)
tcp_server.bind(ip_port)
tcp_server.listen(back_log)
print('服务端开始运行')
conn, addr = tcp_server.accept()
while True:
data = conn.recv(buffer_size)
print('客户端发来的信息是', data.decode('utf-8'))
conn.send(data.upper())
conn.close()
tcp_server.close()
服务端开始运行
客户端发来的信息是 python
客户端:
from socket import *
ip_port = ('127.0.0.1', 8000)
buffer_size = 1024
tcp_client = socket(AF_INET, SOCK_STREAM)
tcp_client.connect(ip_port) # 连接服务端
while True:
msg = input('请输入要发送的信息:')
tcp_client.send(msg.encode('utf-8'))
print('---------客户端已经发送消息------------')
data = tcp_client.recv(buffer_size)
print('接收到服务端发来的信息', data.decode('utf-8'))
tcp_client.close()
请输入要发送的信息:python
---------客户端已经发送消息------------
接收到服务端发来的信息 PYTHON
请输入要发送的信息:
8. socket 收发信息原理剖析
socket 客户端和服务端都属于应用层,即用户态层面。它们产生的数据(或从客户端发送到服务端的数据)必须通过内核态调用操作系统,将数据 copy 到内存中(缓存),然后根据 TCP/UDP 协议、通过网卡、Internet 传输到服务端。
服务端再通过内核态从缓存中取出数据。收发消息会在缓存中形成一个消息队列,遵循 先进先出原则,后面进来的消息后处理。
操作系统的体系架构分为 用户态和内核态,内核从本质上讲也一种软件 —— 控制计算机的硬件资源,并提供上层应用程序运行的环境。
用户态即上层应用程序的活动空间,应用程序的执行必须依托内核提供的资源,包括(CPU、存储、I/O资源等)。为了使用户态能访问这些资源,内核必须为上层应用提供访问的接口 —— 系统调用(系统调用是操作系统的最小单位)。
9. 服务端循环连接请求来接收信息
9.1 当用户输入为空或直接回车时
当用户在 client 端输入为空,或直接输入回车时,client 端与 server 端都开在接收信息处。这是因为 client 端没有真正的信息(0 字节)发送给 server 端,因此 server 端就不会有信息回复。
解决办法:在 client 端对用户输入的信息进行判断即可
if not msg:continue # 如果输入信息为空,那么继续输入
9.2 当 client 端异常断开时
当我们直接断开 client 的连接,而非四次挥手时正常断开,发现 server 直接报如下错误:
ConnectionResetError: [WinError 10054] 远程主机强迫关闭了一个现有的连接。
现实情况中,不可能一个 client 端每次不正常断开连接,就导致 server 端直接断开。那么其他的 client 就不能连接 server。
解决办法:对 server 端信息接收处使用异常处理
while True:
try:
data = conn.recv(buffer_size)
print('客户端发来的信息是', data.decode('utf-8'))
conn.send(data.upper())
except Exception:
break
这样不论 client 端是怎么断开的,都不会导致 server 端断开。
9.3 当有多个 client 发起连接时
当有多个 client 发起连接时,遵循 先进先出 原则。先连接的 client ,先处理,后发起连接的 client 会被存储到 back_log 中。back_log 为链接监听(listen)的最大数目。
每次只能处理一天连接,当处理完毕后就会直接关闭连接,也就是说只能服务一个 client,我们希望的是 server 端能够循环提供服务,显然这不是我们想要的结果。
解决办法:对被动接受 client 的连接处进行循环(即 accept)
服务端:
from socket import *
# 获取主机名
host = gethostname()
# 端口号
port = 8080
back_log = 5
buffer_size = 1024
tcp_server = socket(AF_INET, SOCK_STREAM)
tcp_server.bind((host, port))
tcp_server.listen(back_log)
while True: # 循环接收 client 发起的连接
print('服务端开始运行')
print(host)
conn, addr = tcp_server.accept()
while True: # 循环接收 client 发来的信息,以及发送信息给 client
try: # 对 client 的异常断开进行异常处理
data = conn.recv(buffer_size)
print('客户端发来的信息是', data.decode('utf-8'))
conn.send(data.upper())
except Exception:
break
conn.close()
tcp_server.close()
客户端:
from socket import *
# 获取主机名
host = gethostname()
# 端口号
port = 8080
buffer_size = 1024
tcp_client = socket(AF_INET, SOCK_STREAM)
tcp_client.connect((host, port)) # 连接服务端
while True:
msg = input('请输入要发送的信息:').strip()
if not msg: continue # 对用户输入的信息进行判断
tcp_client.send(msg.encode('utf-8'))
print('---------客户端已经发送消息------------')
data = tcp_client.recv(buffer_size)
print('接收到服务端发来的信息', data.decode('utf-8'))
tcp_client.close()
9.5 总结
要想 client 与 server 能够自由交互数据,并且 server 能循环提供服务,需要满足如下条件:
- 需要对用户输入的数据进行判断
- server 能够处理 client 异常断开时的情况
- server 要能够循环接收 client 发起的连接
10. socket 函数
1. 服务端套接字函数
- s.bind():绑定(主机,端口号)到套接字,元组形式
- s.listen():开始 TCP 监听
- s.accept():被动接受 TCP 客户的连接,(阻塞式)等待连接的到来。
2. 客户端套接字函数
- s.connect():主动初始化 TCP 服务器连接
- s.connect_ex():connect()函数的拓展版本,出错时返回出错码,而不是抛出异常
3. 公共用途套接字函数
- s.recv():接受 TCP 数据
- s.send():发送 TCP 数据(send 在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完)
- s.sendall():发送完整的 TCP 数据(本质就是循环调用 send,sendall 在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用 send 直到发完。)
- s.recvfrom():接收 UDP 数据
- s.sendto():发送 UDP数据
- s.getpeername():连接到当前套接字的远端的地址
- s.getsockname():当前套接字的地址
- s.getsockopt():返回指定套接字的参数
- s.setsockopt():设置指定套接字的参数
- s.close():关闭套接字
面向锁的套接字函数
- s.setblocking():设置套接字的阻塞与非阻塞模式
- s.settimeout():设置阻塞套接字操作的超时时间
- s.gettimeout():得到阻塞套接字操作的超时时间
面向文件的套接字的函数
- s.fileno():套接字的文件描述符
- s.makefile():创建一个与该套接字相关的文件
**Tips:**send 一次最大数据最好控制在 8 k 左右,为了避免超过内存大小,可以使用 sendall 方法。其本质是在循环调用 send 方法。