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 方法。



















