TCP 简介
1. TCP 介绍
TCP 协议,传输控制协议(英语:Transmission Control Protoco,缩写 TCP)是一种 面向连接的、可靠的、基于字节流 的传输层通信协议,由 IETF 的 RFC 793定义。
TCP 通信需要经过 创建连接、数据传递、终止连接 三个步骤。
TCP 通信模型中,在通信开始之前,一定要先建立相关的链接,才能发送数据,类似于生活中 "打电话"。
2. TCP 特点
2.1 面向连接
通信双方必须先建立连接才能进行数据的传输,双方都必须为该连接分配必要的系统内核资源,以管理连接的状态的连接上的传输。
双方间的数据传输都可以通过这一个连接进行。完成数据交换后,双方必须断开此连接,以释放系统资源。这种连接是一对一的,因此 TCP不适用于广播的应用程序,基于广播的应用程序请使用 UDP 协议。
2.2 可靠传输
1)TCP 采用发送应答机制
TCP 发送的每个报文段都必须得到接收方的应答才认为这个 TCP 报文段传输成功。
2)超时重传
发送端发出一个报文段之后就启动定时器,如果在定时时间内没有收到应答就重新发送这个报文段。TCP 为了保证不发生丢包,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的包发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据包就被假设为已丢失将会被进行重传。
3)错误校验
TCP 用一个校验和函数来检验数据是否有错误;在发送和接收时都要计算校验和。
4)流量控制和阻塞管理
流量控制用来避免主机发送的过快而使接收方来不及完全收下。
TCP 与 UDP 的不同点
- 面向连接(确认有创建三方交握,连接已创建才作传输)
- 有序数据传输
- 重发丢失的数据包
- 舍弃重复的数据包
- 无差错的数据传输
- 阻塞 / 流量控制
3. UDP 通信模型
UDP 通信模型中,在通信开始之前,不需要建立相关的链接,只需要发送数据即可,类似于生活中的 "写信"。
4. TCP 客户端
所谓的服务器端:就是提供服务的一方,而客户端:就是需要被服务的一方。
4.1 客户端构建流程
TCP 的客户端要比服务器端简单的多,如果说服务器端是需要自己买手机、插手机卡、设置铃声、等待别人打电话流程的话,那么客户端就只需要找一个电话亭,拿起电话拨打即可,流程要少很多。
示例代码:
import socket
def main():
# 1.创建tcp的套接字
tcp_socket =socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2.链接服务器
server_ip = input("请输入要链接的服务器的ip:")
server_port = int(input("请输入要链接的服务器的port:"))
server_addr = (server_ip, server_port)
tcp_socket.connect(server_addr)
# 3.发送数据/接收数据
send_data = input("请输入要发送的数据:")
tcp_socket.send(send_data.encode("gbk"))
# 4.关闭套接字
tcp_socket.close()
if __name__ == "__main__":
main()
5. TCP 服务器
5.1 生活中的手机
如果想让别人能够打通咱们的电话获取相应服务的话,需要做以下几件事情:
- 买个手机
- 插上手机卡
- 设计手机为正常接听状态(即能够响铃)
- 静静的等着别人拨打
5.2 TCP 服务器
如同上面的电话机过程一样,在程序中,如果想要完成一个 TCP 服务器 的功能,需要的流程如下:
- socket 创建一个套接字
- bind 绑定 ip 和 port
- listen 使套接字变为可以被动链接
- accept 等待客户端的链接
- recv / send接收发送数据
一个很简单的 TCP 服务器如下:
import socket
def main():
# 1.买个手机(创建套接字 socket)
tcp_server_socket = socket.socket(socket.AF_INET,
socket.SOCK_STREAM)
# 2.插入手机卡(绑定本地信息 bind)
tcp_server_socket.bind(("", 7890))
# 3.将手机设置为正常的响铃模式(让默认的套接字由主动变为被动 listen)
# 创建出来的套接字默认是链接别人,不是别人链接你
tcp_server_socket.listen(128)
print("------1------")
# 4.等待别人的电话到来(等待客户端的链接 accept)
new_client_socket, client_addr = tcp_server_socket.accept()
"""
accept 有一个返回值,并且返回值是一个元组。如果等号的右边是一个元组,
等号的左边是两个变量,这叫拆包!
"""
print("------2------")
"""
accept()接受一个客户端的链接请求,并返回一个新的套接字,
不同于以上socket.socket()返回的是用于监听和接受客户端的链接请求的
套接字;与此客户端通信是通过这个新的套接字上发送和接收数据来完成的。
每个链接进来的客户端,都会通过accept函数返回一个不同的客户端的
socket 对象和属于客户端的套接字
"""
# new_client_socket 用来接收一个新的套接字,
# client_ddr 用来接收一个链接你的客户端的地址
# 监听套接字负责等待有新的客户端进行链接,
# accept 产生的新的套接字用来为客户端服务
# 谁给你打的电话
print(client_addr)
# 服务器接收客户端发送过来的请求
recv_data = new_client_socket.recv(1024)
print(recv_data)
# 回送一部分数据给客户端
new_client_socket.send("收到请求...".encode("gbk"))
# 关闭套接字
new_client_socket.close()
tcp_server_socket.close()
if __name__ == "__main__":
main()
没有显示 ------2------ ,说明堵塞,等别人来的时候才会解堵塞。什么时候解堵塞?也就是说有一个新的客户端链接你就会解堵塞(也就是说只要一个客户端调用 connect,accept 就会解堵塞,不仅解堵塞,解完堵塞 accept 还有一个返回值,返回值是一个元组,里面分为两部分).
accept 的返回值是一个元组,元组里面有两个参数。为了更好地去利用这个元组的返回值,直接写成两个变量的拆包。第一个参数 new_client_socket 用来接收一个新的套接字,第二个参数 client_ddr 用来接收一个链接你的客户端的地址。还拿打电话的例子来说,谁给你打的电话啊,就存放在 client_ddr 里面,为这个客户服务的时候呢,找了一个新的套接字。怎么更好地理解呢?
我们可以把最开始创建的套接字 tcp_server_socket 理解成某个公司买的一个比较大的电话机,这个电话机用的端口是 7890,也就是相当于这个电话机向电信局申请了一个号码 7890,因为这个电话机比较大,是公司里面的电话机,和家用的不一样。不太一样他想做什么呢?它想做一个客服系统。什么叫客服?你给 10086 打电话,打的是 10086,将来真正为我们服务的是谁?是客服。
人工服务,找一个人为这个客户,他是怎么办的呢?
你打电话的时候, accept 就有响应, accept 响应的的结果就是一看到这个人给我打电话,就找了个客服,为这个客户端服务。为这个客户端服务的时候,设想一个日常生活中的场景。你和客服人员说,您好,我想把我的套餐升级一下,客服往往会说,先生咱们接下来确认一下身份信息,请问您是 xxxx 的机主么?客服是怎么知道的你的号码?你给10086 电话,那里面就有来电显示,找客服为你服务的时候,就把来电显示号码顺便告诉了客服人员,这个客服人员 就得到了号码,得到了号码现在有几个东西为这个客户端服务呢?一个客服为他服务,客服也知道这个客户的号码(ip)是谁,因此 accept 的返回值是一个元组,这个元组里面有两部分。第一部分找了一个客服(tcp_server_socket),就是套接字,第二部分客户端连接你的时候,客户端的地址(tcp_addr)。
服务器10086 这个系统先买了一个电话机,这个电话机默认用来打电话的,先 listenー下,tcp_server_socket 就变成了一个被动的 套接字 了。只要做好了之后,这个套接字就可以等待别人给你打电话了。那么这个时候,客户往套接字( tcp_server_socket)里面打电话,这个套接字 tcp_server_socket 对应的端口就是 7890,所以说这个套接字可以接收来自7890 端口的数据了,收到了数据之后,通过这个套接字( tcp_server_socket) accept,电话机( tcp_server_socket)是用来单独等待别人打电话的,为了服务这个客户,又找了一个新的套接字( new_client_socket)这个新的套接字为这个客户服务,而上面的那个箭头至始至终就没了。
如果这个时候又来了个新的客户端(客户2),这个人也需要打 10086,他会往创建的监听套接字里面打,监听套接字创建完之后,找个客服为客户2服务,他又找了一个客服,即又找了一个套接字,这个套接字为客户2服务,tcp 通信的大体是,服务器先创建一个套接字,放在那等着,来一个客户端给服务器打电话,就找一个客服为这个客户服务,客服拿着信息走了,就是这个道理!
有一个客户2打电话,10086 又找了另一个客服,客服为客户2服务。服务器作为 tcp 的服务器方,客户端打电话往服务器的监听套接字里面打,将来服务器会通过 accept 得到一个新的套接字,这个新的套接字标记了这个客户,有多少个客户端,直白的讲服务器里面就有多少个新的套接字。
接下来客户端所发送的所有数据以及接收到的所有数据,通通都是通过新的套接字来的。监听套接字(tcp_server_socket)只负责等待别人打电话,新的套接字(new_client_socket)负责通信。
监听套接字默认会堵塞,堵塞到什么时候为止?有一个新的客户端链接你,你就会解堵塞,而且解堵塞的时候有个返回值(new_client_socket, client_addr)。
这种 拆包 有个特点,后面的元组有多少个元素,前面就多少个变量,这种方式适合你得到的元组里面的元素个数是固定的。
代码升级(循环为多个客户端的服务器.py):
import socket
def main():
# 1.买个手机(创建套接字 socket)
tcp_server_socket = socket.socket(socket.AF_INET,
socket.SOCK_STREAM)
# 2.插入手机卡(绑定本地信息 bind)
tcp_server_socket.bind(("", 7890))
tcp_server_socket.listen(128)
while True:
print("等待客户端的到来...")
# 4.等待别人的电话到来(等待客户端的链接 accept)
new_client_socket, client_addr = tcp_server_socket.accept()
print("一个新的客户端已经到来 %s" % str(client_addr))
# 服务器接收客户端发送过来的请求
recv_data = new_client_socket.recv(1024)
print("客户端发送过来的请求是 %s" % recv_data.decode("gbk"))
# 回送一部分数据给客户端
new_client_socket.send("收到请求...".encode("gbk"))
# 关闭套接字
# 关闭accept返回的套接字 意味着 不会再为这个客户端服务了
new_client_socket.close()
print("服务完毕!")
# 如果将监听套接字关闭了,那么会导致 不能再次等待新客户的到来,
# 即xxxx.accept就会失败
tcp_server_socket.close()
if __name__ == "__main__":
main()
再升级后(循环为多个客户端服务器并且多次服务一个客户端.py):
import socket
def main():
# 1.买个手机(创建套接字 socket)
tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2.插入手机卡(绑定本地信息 bind)
tcp_server_socket.bind(("", 7890))
# 3.将手机设置为正常的响铃模式(让默认的套接字由主动变为被动 listen)
# 创建出来的套接字默认是链接别人,不是别人链接你
tcp_server_socket.listen(128)
# 这个while True循环为多个客户端服务
while True:
print("等待客户端的到来...")
# 4.等待别人的电话到来(等待客户端的链接 accept)
new_client_socket, client_addr = tcp_server_socket.accept()
print("一个新的客户端已经到来 %s" % str(client_addr))
# new_client_socket 用来接收一个新的套接字,clientAddr 用来接收一个链接你的客户端的地址
# 监听套接字负责等待有新的客户端进行连接,accept 产生的新的套接字用来为客户端服务
# 谁给你打的电话
# 这个while True循环多次为同一个客户端服务多次
while True:
# 服务器接收客户端发送过来的请求
recv_data = new_client_socket.recv(1024)
print("客户端发送过来的请求是 %s" % recv_data.decode("gbk"))
# 如果recv解堵塞,那么有两种方式:
# 1.客户端发送过来数据
# 2.客户端调用close导致了这里recv解堵塞
if recv_data:
# 回送一部分数据给客户端
new_client_socket.send("收到请求...".encode("gbk"))
else:
break
# 关闭套接字
new_client_socket.close()
print("服务完毕!")
tcp_server_socket.close()
if __name__ == "__main__":
main()
TCP 注意点:
- TCP 服务器一般情况下都需要绑定,否则客户端找不到这个服务器。
- TCP 客户端一般不绑定,因为是主动链接服务器,所以只要确定好服务器的 ip、port 等信息就好,本地客户端可以随机
- TCP 服务器中通过 listen 可以将 socket 创建出来的主动套接字变为被动的,这是做 TCP 服务器时必须要做的
- 当客户端需要链接服务器时,就需要使用 connect 进行链接,UDP 是不需要链接的而是直接发送,但是 TCP必须先链接,只有链接成功才能通信
- 当一个 TCP客户端连接服务器时,服务器端会有 1 个新的套接字,这个套接字用来标记这个客户端,单独为这个客户端服务
- listen 后的套接字是被动套接字,用来接收新的客户端的链接请求的,而 accept返回的新套接字是标记这个新客户端的
- 关闭 listen 后的套接字是意味着被动套接字关闭了,会导致新的客户端不能够链接服务器,但是之前已经链接成功的客户端正常通信
- 关闭 accept 返回的套接字意味着这个客户端已经服务完毕
- 当客户端的套接字调用 close 后,服务器端会 recv 解堵塞,并且返回的长度为 0,因此服务器可以通过返回数据的长度来区分客户端是否已经下线
解堵塞有两种方式:
1. 收到了消息解堵塞
2. 对方调用了 close
recv 和 recvfrom 有什么区别?
recvfrom 里面不仅有数据,还有谁发过来的信息;
recv 里面只有数据,因为之前已经知道这个客户端的信息是谁了。