假设我们需要编写一个C/S架构的程序,实现数据交互,就需要使用到OSI七层协议,由于它的缺点是分层太多
增加了网络工作的复杂性,所以没有大规模应用。后来人们对 OSI 进行了简化,合并了一些层,最终只保留
了 4 层,从下到上分别是接口层、网络层、传输层和应用层,这就是 TCP/IP 模型。
1. 而socket(套接字)是在应用程序的传输层和应用层之间抽象出了一个层叫做socket抽象层
2. 可以理解为TCP/IP协议栈提供的对外的操作接口,即应用层通过网络协议进行通信的接口。
3. Socket其实就是一个门面,它把复杂的TCP/IP协议族隐藏在Socket接口后面,不需要自己处理每一层
基于文件类型的套接字家族
套接字家族的名字:AF_UNIX
unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,
可以通过访问同一个文件系统间接完成通信
基于网络类型的套接字家族
套接字家族的名字:AF_INET
AF_INET(又称 PF_INET)是 IPv4 网络协议的套接字类型,AF_INET6 则是 IPv6 的
还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,很少被使用,或者是根
本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只
关心网络编程,所以大部分时候我么只使用AF_INET
套接字Socket可以使用不同的网络协议进行端对端的通信,主要是TCP和UDP协议,使用的流程如下图所示
在python中socket就是一个模块。通过调用模块中的方法建立两个进程之间的连接和通信。
也有人将socket说成ip+port,因为ip是用来标识互联网中的一台主机的位置,而port是用来标识这台机器
上的一个应用程序。所以我们只要确立了ip和port就能找到一个应用程序,并且使用socket模块来与之通信
基于TCP协议的socket需要先建立链接,首先肯定是编写服务端再去考虑客户端。
TCP服务端和客户端基础代码
'''TCP服务端基础代码''' 代码示例 import socket server = socket.socket() # 创建服务端套接字对象,默认为基于网络的遵循TCP协议的套接字(相当于买了一个手机) server.bind(('127.0.0.1', 8088)) # 将地址绑定给套接字(相当于插上了电话卡) server.listen(5) # 开始监听传入链接(相当于手机开机) sock, addr = server.accept() # 接受客户端链接并且会返回俩个值,其中sock是客户端的消息,addr是客户端的地址(相当于等待接听电话) print(addr) # 打印客户端的地址 # listen 和 accept 分别代表着 TCP 中的 LISTEN、RCVD 俩个状态。 data = sock.recv(1024) # 接收客户端的信息,信息是二进制格式(相当于听别人说话) print(data.decode('utf8')) # 打印客户端信息 sock.send('hello'.encode('utf8')) # 向客户端发送信息,发送的也是二进制格式(相当于回复别人说的话) sock.close() # 关闭客户端套接字(相当于挂电话) server.close() # 关闭服务端套接字(相当于关机)
'''TCP客户端基础代码''' 代码示例 import socket client = socket.socket() # 创建客户端套接字对象(相当于买手机) client.connect(('127.0.0.1', 8088)) # 根据服务端的地址链接服务端 client.send(b'hello server') # 向服务端发送信息 data = client.recv(1024) # 接收服务端发送的信息 print(data.decode('utf8')) # 打印消息 client.close() # 关闭客户端套接字
需要注意的是服务端与客户端首次交互,一边是recv那么另一边必须是send,都是recv程序会一直停住
.
基础代码完成后,先运行服务端在运行客户端,此时客户端就会向服务端发送信息。
但是此时信息为空的时候,双方都会进入recv导致程序卡住,而且如果需要多条消息且自定义消息,就需要改进通信循环
TCP服务端和客户端改进通信循环
'''TCP服务端通信循环代码''' 代码示例 import socket server = socket.socket() # 创建服务端套接字对象,默认为基于网络的遵循TCP协议的套接字(相当>于买了一个手机) server.bind(('127.0.0.1', 8088)) # 将地址绑定给套接字(相当于插上了电话卡) server.listen(5) # 开始监听传入链接(相当于手机开机) sock, addr = server.accept() # 接受客户端链接并且会返回俩个值,其中sock是客户端的消息,addr是客户端的地址(相当于等待接听电话) print(addr) # 打印客户端的地址 # listen 和 accept 分别代表着 TCP 中的 LISTEN、RCVD 俩个状态。 while True: # 添加循环 data = sock.recv(1024) # 接收客户端的信息,信息是二进制格式(相当于听别人说话) print(data.decode('utf8')) # 打印客户端信息 msg = input('请输入要回复的消息: ').strip() if len(msg) == 0: msg = '自动回复' sock.send(msg.encode('utf8')) # 向客户端发送信息,发送的也是二进制格式(相当于回复别人说的话) sock.close() # 关闭客户端套接字(相当于挂电话),在循环下不执行 server.close() # 关闭服务端套接字(相当于关机),在循环下不执行
'''TCP客户端通信循环代码''' 代码示例 import socket client = socket.socket() # 创建客户端套接字对象(相当于买手机) client.connect(('127.0.0.1', 8088)) # 根据服务端的地址链接服务端 while True: msg = input('请输入要发送的信息: ').strip() if len(msg) == 0: continue client.send(msg.encode('utf8')) # 向服务端发送信息 data = client.recv(1024) # 接收服务端发送的信息 print(data.decode('utf8')) # 打印消息 client.close() # 关闭客户端套接字,在循环下不执行
现在的代码可以输入自定义信息以及循环发送信息,但是我们将服务端快速重启有可能会报下面的错误
这是由于快速关闭端口还没来得及回收导致的,我们需要在加俩句固定代码
from socket import SOL_SOCKET,SO_REUSEADDR
sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) # 这句话加在bind前面
TCP服务端和客户端改进链接循环
如果是 windows 客户端异常退出之后其服务端会直接报错
如果是 mac 或 linux 客户端异常退出的话其服务端会接收到一个空消息'''TCP服务端链接循环代码''' 代码示例 import socket server = socket.socket() # 创建服务端套接字对象,默认为基于网络的遵循TCP协议的套接字(相当于买了一个手机) server.bind(('127.0.0.1', 8088)) # 将地址绑定给套接字(相当于插上了电话卡) server.listen(5) # 开始监听传入链接(相当于手机开机) while True: sock, addr = server.accept() # 接受客户端链接并且会返回俩个值,其中sock是客户端的消息,addr是客户端的地址(相当于等待接听电话) print(addr) # 打印客户端的地址 # listen 和 accept 分别代表着 TCP 中的 LISTEN、RCVD 俩个状态。 while True: try: data = sock.recv(1024) # 接收客户端的信息,信息是二进制格式(相当于听别人说话) print(data.decode('utf8')) # 打印客户端信息 msg = input('请输入要回复的消息: ').strip() if len(msg) == 0: msg = '自动回复' sock.send(msg.encode('utf8')) # 向客户端发送信息,发送的也是二进制格式(相当于回复别人说的话) except ConnectionResetError as e: print('客户端异常退出') break
TCP服务端代码不变
此时就算客户端异常退出,重新连接也可以建立链接,而且地址是不一样的。
server端
import socket
udp_sk = socket.socket(type=socket.SOCK_DGRAM) #创建一个套接字对象
udp_sk.bind(('127.0.0.1',9000)) #绑定服务器地址
msg,addr = udp_sk.recvfrom(1024) # 接收发送方的消息以及地址
print(msg)
udp_sk.sendto(b'hi',addr) # 发送消息,需要加上接收地址
udp_sk.close() # 关闭服务器套接字
client端
import socket
ip_port=('127.0.0.1',9000) # 设置发送地址
udp_sk=socket.socket(type=socket.SOCK_DGRAM) # 创建套接字对象
udp_sk.sendto(b'hello',ip_port) # 发送消息,需要加上接收方地址
back_msg,addr=udp_sk.recvfrom(1024) # 接收发送方的消息和地址
print(back_msg.decode('utf-8'),addr)
半连接池就是设置等待的最大链接个数。
在 listen(5) 括号里的数字中体现,括号里的数字就是等待链接的最大个数,用于节省资源
超出最大数量的客户端会报错,停止运行。只有前一个停止链接下一个才会接上
1. 在上面的代码截图中,客户端向服务端发送了三个数据按理来说会依次接收,但其实结果如下图所示.
2. 这个问题就叫做黏包问题。TCP协议也被称之为流式协议,这是因为它会将数据量比较小并且时间间隔比较短的数据整合到一起发送,并且还会受制于recv括号内的数字大小,也就是说数据小于括号中的字节大小就会一起发送,但是我们并不需要它们整合在一起,就需要精准的知道发送的数据的大小那么这个问题就解决了。
.
3. 那么如何精准的获取数据的大小呢,我们可以借助于一个模块,这个模块就是 struct 模块,
我们可以利用 struct 模块的小特性来精准的知道数据的大小
struct的 pack 可以将任意长度的数字打包成固定长度
代码示例
import struct
x = 'hello world'
print('hello 的长度是: ', len(x))
y = struct.pack('i', len(x)) # 第一个参数是格式格式如下图
print('打包后的长度是: ', len(y))
x1 = 'XWenXiang'
print('\nXWenXiang 的长度是: ', len(x1))
y1 = struct.pack('i', len(x1))
print('打包后的长度是: ', len(y1))
输出结果
hello 的长度是: 11
打包后的长度是: 4
XWenXiang 的长度是: 9
打包后的长度是: 4
可以发现不同的长度被打包过后的长度都是一样的,也就是说 pack 可以将任意长度的数字打包成固定长度
需要注意的是,例如格式i都有它的表示范围。
struct.unpack()的作用就是将固定长度的数字解包成打包前真实的长度
代码示例
import struct
x = 'hello world'
print('hello 的长度是: ', len(x))
y = struct.pack('i', len(x))
print('打包后的长度是: ', len(y))
z = struct.unpack('i', y)
print('解包后的长度: ', z)
打印结果
hello 的长度是: 11
打包后的长度是: 4
解包后的长度: (11,)
发送方:利用struct模块对信息长度进行打包,并将打包的数据和真实信息发出
接收方:接收打包的数据并解包成真实信息长度,利用这个长度来接收真实信息。
这样接收的数据长度就是准确的了。
服务端代码
服务端代码
import socket
import struct
server = socket.socket()
server.bind(('127.0.0.1', 8088))
server.listen(5)
sock, addr = server.accept()
long_a = sock.recv(4) # 获取客户端发送的第一条数据,也就是打包后的长度,固定为4
long_b = struct.unpack('i', long_a)[0] # 将打包后固定的长度转换成真实的信息长度,由于是元组,通过索引取值。
data = sock.recv(long_b) # 接收客户端发送的第二条数据,也就是真实的信息,由于recv中的数据是准确的,所以不会出错
print(data.decode('utf8')) # 打印真实信息,将二进制转换
sock.close()
server.close()
客户端代码
客户端代码
import socket
import struct
client = socket.socket()
client.connect(('127.0.0.1', 8088))
str_a = 'XWenXiang'.encode('utf8') # 必须为二进制形式
str_a_long = struct.pack('i', len(str_a)) # 将真实信息的长度打包得到一个固定长度的数据
client.send(str_a_long) # 将固定长度的数据发送给服务端
client.send(str_a) # 再将真正的信息发送出去
client.close()
如果信息数据太大,我们可以将它组成字典,通过字典不仅能存放数据的长度还能存放额外的信息。
下面的示例是从服务端传文件给客户端。
服务端代码
服务端代码
import socket
import struct
import os
import json
server = socket.socket()
server.bind(('127.0.0.1', 8088))
server.listen(5)
sock, addr = server.accept()
while True:
data_dict = { # 创建字典
'file_name': 'abc.txt',
'file_desc': '文件abc',
'file_size': os.path.getsize('a/abc.txt'), # 返回 path 的大小,以字节为单位
}
# 1. 打包字典
dict_json = json.dumps(data_dict) # 将字典转换成json格式的字符串
dict_bytes = dict_json.encode('utf8') # 将json格式的字符串转换成字节型
dict_package_header = struct.pack('i', len(dict_bytes)) # 打包字典获取固定数据
# 2. 发送报头
sock.send(dict_package_header) # 发送固定长度的数据
# 3. 发送字典
sock.send(dict_bytes) # 发送真实字典
# 4. 循环发送文件
with open('a/abc.txt', 'rb') as f:
for line in f :
sock.send(line)
客户端代码
客户端代码
import socket
import struct
import json
client = socket.socket()
client.connect(('127.0.0.1', 8088))
dict_hader_len = client.recv(4) # 接收服务端发送的第一条数据也就是打包字典后的固定长度数据
dict_real_len = struct.unpack('i', dict_hader_len)[0] # 解包获得真实字典的长度
dict_date_bytes = client.recv(dict_real_len) # 根据真实的字典长度获取字典的数据
dict_data = json.loads(dict_date_bytes) # 将json格式的字典准换成普通字典
print(dict_data)
# 循环获取文件
recv_size = 0
with open(dict_data.get('file_name'), 'wb') as f:
while recv_size < dict_data.get('file_size'):
data = client.recv(1024)
recv_size += len(data)
f.write(data)