网络编程(二)

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

风老魔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值