基于TCP协议和UDP协议的socket简单通信与文件下载
Socket是什么呢?
答:Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
总结一下就是:socket就是一个模块。我们通过调用模块中已经实现的方法建立两个进程之间的连接和通信。也有人将socket说成ip+port,因为ip是用来标识互联网中的一台主机的位置,而port是用来标识这台机器上的一个应用程序。 所以我们只要确立了ip和port就能找到一个应用程序,并且使用socket模块来与之通信。如果直接与操作系统数据交互非常麻烦,繁琐,socket对这些繁琐的的操作高度的封装,简化
(一)基于TCP的通讯循环
(一)初级版
话不多说,上代码
服务端
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)#不填参数的话,默认就是基于网络的TCP的通讯
#服务器端先初始化Socket,socket.AF_INET这是基于网络的,socket.SOCK_STREAM这是基于TCP的
server.bind(('127.0.0.1', 10001))#然后绑定IP和端口
server.listen(5)#对端口进行监听,表示最多可以有5个客户端进入半连接持,所以是最多同时保持与6个客户端的连接
conn, client_addr = server.accept()#被动接受TCP客户的连接,等待连接的到来
print(f'与客户端{client_addr}连接,等待客户端发消息')
while 1: # 循环收发消息
from_client_data = conn.recv(1024)#这里表示最多一次从缓冲区接受1024个字节
print(f'\033[0;31m 来自客户端{client_addr}的消息:{from_client_data.decode("utf-8")} \033[0m')#这里送给字体加上了颜色
to_client_data=input('>>>').strip().encode('utf-8')
conn.send(to_client_data)#发送TCP数据
conn.close()#关闭通信管道
server.close()#关闭整个通讯服务
客户端
import socket
client = socket.socket()#默认就是基于TCP的网洛通讯协议
client.connect(('127.0.0.1', 10001)) # 与服务端建立连接
while 1: # 循环收发消息
to_server_data = input('>>>').strip().encode('utf-8')
client.send(to_server_data)
from_server_data = client.recv(1024).decode('utf-8')
print(f'\033[0;31m 来自服务端的消息:{from_server_data} \033[0m')
client.close() # 关闭通讯服务
这里需要注意的是:这里需要先启动服务端的代码,不然会报下面的错误
当然在也很好理解,客户端启动后找不到服务器端开启的端口服务,当然会报错
下面贴上正常运行的图片
客户端
服务端
这时如果我强行中断客户端程序,如果这时服务器端再给客户端发消息就会报错
(二)解决客户端和服务端强行中断的错误并添加一个功能
这里解决上面服务端未打开,客户端打开后报错的问题和强行中断客户端程序报错的问题,并添加一个功能(就是在客户端输入exit,客户端就退出通讯,并且服务端提示,客户端正常退出)
话不多说,上代码
服务端
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 不填参数的话,默认就是基于网络的TCP的通讯
# 服务器端先初始化Socket,socket.AF_INET这是基于网络的,socket.SOCK_STREAM这是基于TCP的
server.bind(('127.0.0.1', 10001)) # 然后绑定IP和端口
server.listen(5) # 对端口进行监听,表示最多可以有5个客户端进入半连接持,所以是最多同时保持与6个客户端的连接
conn, client_addr = server.accept() # 被动接受TCP客户的连接,等待连接的到来
print(f'与客户端{client_addr}连接,等待客户端发消息')
while 1: # 循环收发消息
try:
from_client_data = conn.recv(1024) # 这里表示最多一次从缓冲区接受1024个字节
if from_client_data == b'exit':
print('客户端正常退出通讯')
break
print(f'\033[0;31m 来自客户端{client_addr}的消息:{from_client_data.decode("utf-8")} \033[0m') # 这里送给字体加上了颜色
to_client_data = input('>>>').strip().encode('utf-8')
conn.send(to_client_data) # 发送TCP数据
if to_client_data == b'exit':
break
except ConnectionResetError:
print('客户端链接中断了')
break
conn.close() # 关闭通信管道
server.close() # 关闭整个通讯服务
客户端
import socket
client = socket.socket() # 默认就是基于TCP的网洛通讯协议
try:
client.connect(('127.0.0.1', 10001)) # 与服务端建立连接
while 1: # 循环收发消息
to_server_data = input('>>>').strip().encode('utf-8')
client.send(to_server_data)
if to_server_data == b'exit':
break
from_server_data = client.recv(1024)
if from_server_data == b'exit':
print('服务端正常退出通讯')
break
print(f'\033[0;31m 来自服务端的消息:{from_server_data.decode("utf-8")} \033[0m')
client.close() # 关闭通讯服务
except ConnectionRefusedError:
print('服务端程序尚未启动,请先打开服务端程序')
except ConnectionResetError:
print('服务端链接中断了')
下面就是先打开客户端程序,服务端程序未打开的情况
下面这两张是客户端输入exit后,运行的结果
(三)解决直接回车程序卡死的问题,并且给程序添加服务端与多个客户端通讯的功能
首先为什么在直接回车后程序会卡死?
答:这里假设如果在客户端输入回车,那么就相当于客户端给服务端发送了一个**‘’,之后客户端程序就进入接受状态(from_server_data = client.recv(1024)),而服务端在接收到这个’'**后,从代码的逻辑上来说, from_client_data = conn.recv(1024)并未接收到任何数据,所以服务端的程序也会在这卡死,一直等待接收数据
下面是解决方法,并且给程序添加服务端与多个客户端通讯的功能
服务端
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 不填参数的话,默认就是基于网络的TCP的通讯
# 服务器端先初始化Socket,socket.AF_INET这是基于网络的,socket.SOCK_STREAM这是基于TCP的
server.bind(('127.0.0.1', 10001)) # 然后绑定IP和端口
server.listen(5) # 对端口进行监听,表示最多可以有5个客户端进入半连接持,所以是最多同时保持与6个客户端的连接
while 1:
conn, client_addr = server.accept() # 被动接受TCP客户的连接,等待连接的到来
print(f'与客户端{client_addr}连接,等待客户端发消息')
while 1: # 循环收发消息
try:
from_client_data = conn.recv(1024) # 这里表示最多一次从缓冲区接受1024个字节
if from_client_data == b'exit':
print(f'客户端{client_addr}正常退出通讯')
break
print(f'\033[0;31m 来自客户端{client_addr}的消息:{from_client_data.decode("utf-8")} \033[0m') # 这里送给字体加上了颜色
to_client_data = input('>>>').strip()
while not to_client_data:
print('发送内容不能为空,请重新发送')
to_client_data = input('>>>').strip()
continue
conn.send(to_client_data.encode('utf-8')) # 发送TCP数据
if to_client_data == b'exit':
break
except ConnectionResetError:
print(f'客户端{client_addr}链接中断了')
break
conn.close() # 关闭通信管道
server.close() # 关闭整个通讯服务
客户端
import socket
client = socket.socket() # 默认就是基于TCP的网洛通讯协议
try:
client.connect(('127.0.0.1', 10001)) # 与服务端建立连接
while 1: # 循环收发消息
to_server_data = input('>>>').strip()
# 如果发送内容为空,服务端就会一直处于阻塞中,所以无论那一端,都不能输入为空
if not to_server_data:
print('发送内容不能为空,请重新发送')
continue
client.send(to_server_data.encode('utf-8'))
if to_server_data == 'exit':
break
from_server_data = client.recv(1024)
if from_server_data == b'exit':
print('服务端正常退出通讯')
break
print(f'\033[0;31m 来自服务端的消息:{from_server_data.decode("utf-8")} \033[0m')
client.close() # 关闭通讯服务
except ConnectionRefusedError:
print('服务端程序尚未启动,请先打开服务端程序')
except ConnectionResetError:
print('服务端链接中断了')
(二)TCP缓冲区和粘包问题及解决方法
在讨论粘包问题前,先了解一下TCP的缓冲区
1.每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。
2.write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情。
3.TCP协议独立于 write()/send() 函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。
4.read()/recv() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取。
缓冲区的优点:输入缓冲区,输出缓冲区. 存储少量数据,避免网络不稳,造成你传输数据时的卡顿,保持相对平稳,稳定.
什么是粘包?
答:粘包是指发送方发送的若干次数据到接收方时,粘成了一个数据包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。只有TCP有粘包现象,UDP不会。
粘包产生的原因?
答:1。当连续发送较小的数据时,由于tcp协议的nagle算法,会将较小的内容拼接成大的内容,一次性发送到服务器端,因此造成粘包
2.当发送内容较大时,由于服务器端的recv(buffer_size)方法中的buffer_size较小,不能一次性完全接收全部内容,因此在下一次请求到达时,接收的内容依然是上一次没有完全接收完的内容,因此造成粘包现象。
粘包的解决方法?
在每次使用tcp协议发送数据流时,在开头标记一个数据流长度信息,并固定该报文长度(自定义协议).在客户端接收数据时先接收该长度字节数据,判断客户端发送数据流长度,并只接收该长度字节数据,就可以实现拆包,完美解决tcp粘包问题.
PS:这里有一个误区就是,在收发消息时,并不是一收一发。它可能会发一次,收多次,也可能是发多次,收一次
(三)UDP下的socket通讯流程
UDP(User Datagram Protocol)不可靠的、无连接的服务,传输效率高(发送前时延小),一对一、一对多、多对一、多对多、面向报文(数据包),尽最大努力服务,无拥塞控制。使用UDP的应用:域名系统 (DNS);视频流;IP语音(VoIP)。
QQ和微信好像就是基于此的通讯方式,可能有小伙伴就问,那我每次都可以接受到朋友发送的数据和消息,很稳定很可靠啊,那是因为人家底层做的很好
因为其无连接服务的特点,在连接中,任何一方强制断开服务都不会报错,所以它也没有半连接池的概念(server.listen),对于服务端来说,无论客户端发来多少请求,我都照单全收,不像TCP必须建立连接(server.accept)
服务端
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # DGRAM:datagram 数据报文的意思,象征着UDP协议的通信方式
# 基于网络的UDP协议的socket
server.bind(('127.0.0.1', 10000))
while 1:
from_client_data,addr = server.recvfrom(1024) # 阻塞,等待客户来消息
print(f'\033[0;31m 来自客户端{addr}的消息:{from_client_data.decode("utf-8")} \033[0m')
# to_client_data = input('>>>:').strip()
# server.sendto(to_client_data.encode('utf-8'), addr)
客户端
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 基于网络的UDP协议的socket
while 1:
to_server_data = input('>>>:').strip()
client.sendto(to_server_data.encode('utf-8'), ('127.0.0.1', 10000))
from_server_data, addr = client.recvfrom(1024)
print(f'\033[0;31m 来自服务端{addr}的消息:{from_server_data.decode("utf-8")} \033[0m')
(四)文件的下载
import socket
import os
import struct
import json
server = socket.socket()
server.bind(('127.0.0.1', 10000))
server.listen(5)
# listen: 2 允许有两个客户端加到半链接池,超过两个则会报错
while 1:
conn, client_addr = server.accept() # 等待客户端链接我,阻塞状态中
print(f'与客户端{client_addr}连接,等待客户端发消息')
while 1:
try:
from_client_data = conn.recv(1024).decode('utf-8') # 接收命令
if from_client_data == 'exit':
print(f'客户端{client_addr}正常退出通讯')
break
data_size = os.path.getsize(f'{from_client_data}') # 获取上传文件的大小
# 1. 自定义报头
head_dic = {
'file_name': f'{from_client_data}',
'total_size': data_size,
}
# 2. json形式的报头
head_dic_json = json.dumps(head_dic)
# 3. bytes形式报头
head_dic_json_bytes = head_dic_json.encode('utf-8')
# 4. 获取bytes形式的报头的总字节数
len_head_dic_json_bytes = len(head_dic_json_bytes)
# 5. 将不固定的int总字节数变成固定长度的4个字节
four_head_bytes = struct.pack('i', len_head_dic_json_bytes)
# 6. 发送固定的4个字节
conn.send(four_head_bytes)
# 7. 发送报头数据
conn.send(head_dic_json_bytes)
# 8. 发送总数据
with open(f'{from_client_data}', mode='rb') as f:
i = 0
total_data = b''
while i < data_size:
result = f.read(1024)
conn.send(result) # 每次发1024个字节,直到发完
i += 1024
except ConnectionResetError:
print(f'客户端{client_addr}链接中断了')
break
conn.close()
server.close()
import socket
import struct
import json
client = socket.socket()
try:
client.connect(('127.0.0.1', 10000))
while 1:
to_server_data = input('>>>请输入文件名:').strip().encode('utf-8')
if not to_server_data:
# 服务端如果接受到了空的内容,服务端就会一直阻塞中,所以无论哪一端发送内容时,都不能为空发送
print('发送内容不能为空')
continue
client.send(to_server_data)
if to_server_data == b'exit':
break
# 1. 接收固定长度的4个字节
head_bytes = client.recv(4)
# 2. 获得bytes类型字典的总字节数
len_head_dic_json_bytes = struct.unpack('i', head_bytes)[0]
# 3. 接收bytes形式的dic数据
head_dic_json_bytes = client.recv(len_head_dic_json_bytes)
# 4. 转化成json类型dic
head_dic_json = head_dic_json_bytes.decode('utf-8')
# 5. 转化成字典形式的报头
head_dic = json.loads(head_dic_json)
total_data = b''
i = 0
while i < head_dic['total_size']:
total_data += client.recv(1024)
i += 1024
with open('红楼梦.txt', encoding='utf-8', mode='w', newline='') as f: # newline=''清除写文件多加的换行
f.write(total_data.decode('utf-8'))
print(f'\033[0;31m 下载成功\033[0m') # 这里送给字体加上了颜色
client.close()
except ConnectionRefusedError:
print('服务端程序尚未启动,请先打开服务端程序')
except ConnectionResetError:
print('服务端链接中断了')
这里是将文件目录下的1.txt(就是红楼梦小说)文件下载,并将其重命名为红楼梦,文件上传的功能方法是一样的,在此基础上改动并添加即可
(五)结语
如果有什么错误的地方,还请大家批评指正。最后,希望小伙伴们都能有所收获。