Socket通信
Socket概念
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用来实现 进程在网络中通信。
补充:
发送数据时:应用层将产生的数据交给socket,sokcet控制操作系统,数据由操作系统发送出去
接收数据时:数据先到达操作系统内存中,再到应用程序中
TCP协议的socket通信流程
tcp是基于链接的,必须先启动服务端,然后再启动客户端去链接服务端
TCP(Transmission Control Protocol)可靠的、面向连接的协议(eg:打电话)、传输效率低全双工通信(发送缓存&接收缓存)、面向字节流。使用TCP的应用:Web浏览器;电子邮件、文件传输程序。
1.服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。
2.在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接
就建立了。
3.客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,
一次交互结束
普通的客户端与服务端
server端
import socket
sk = socket.socket()
# sk 是套接字对象,用来与客户端建连接的
sk.bind(('127.0.0.1',8898)) #把地址绑定到套接字
sk.listen() #监听链接
conn,addr = sk.accept() #等待接受客户端链接
# conn 是套接字(双向通道通信)对象, addr是ip和port
ret = conn.recv(1024) #等待接收客户端信息
print(ret) #打印客户端信息
conn.send(b'hi') #向客户端发送信息
conn.close() #关闭客户端套接字
sk.close() #关闭服务器套接字(可选)
client端
import socket
sk = socket.socket() # 创建客户套接字
sk.connect(('127.0.0.1',8898)) # 尝试连接服务器
sk.send(b'hello!')
ret = sk.recv(1024) # 对话(发送/接收)
print(ret)
sk.close() # 关闭客户套接字
客户端与服务端的连接循环+通信循环
server端
import socket
'''
服务端有固定的ip和port,要24小时不间断服务客户端
'''
server = socket.socket() # 生成一个对象
server.bind(('127.0.0.1', 8080)) # 绑定ip和port
server.listen(5) # 半连接池
while True: # 连接循环
conn, addr = server.accept() # 当一个客户端断开通道后,等待下一个客户端的连接
while True: # 通信循环
try:
data = conn.recv(1024)
print(data) # mac与linux 客户端异常退出后,服务端并不会报错,会循环打印 b''
if len(data) == 0:break # 用户兼容mac与linux
conn.send(data.upper()) # 将数据大写发送给客户端
except ConnectionAbortedError: # 捕捉 客户端异常退出 的错误信息
break # 当客户端异常退出后结束通信循环
conn.close() # 当客户端异常退出后关闭通道,进入下一次连接循环
client端
import socket
client = socket.socket()
client.connect(('127.0.0.1', 8080))
while True:
msg = input('>>>:').encode('utf-8')
if len(msg) == 0:continue
client.send(msg)
data = client.recv(1027)
print(data)
问题:在重启服务端时可能会遇到报错
#加入一条socket配置,重用ip和端口
import socket
from socket import SOL_SOCKET,SO_REUSEADDR
sk = socket.socket()
sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加
sk.bind(('127.0.0.1',8898)) #把地址绑定到套接字
sk.listen() #监听链接
conn,addr = sk.accept() #接受客户端链接
ret = conn.recv(1024) #接收客户端信息
print(ret) #打印客户端信息
conn.send(b'hi') #向客户端发送信息
conn.close() #关闭客户端套接字
sk.close() #关闭服务器套接字(可选)
UDP协议的socket通信
udp是无链接的,启动服务之后可以直接接受消息,不需要提前建立链接
UDP(User Datagram Protocol)不可靠的、无连接的服务,传输效率高(发送前时延小),一对一、一对多、多对一、多对多、面向报文,尽最大努力服务,无拥塞控制。使用UDP的应用:域名系统 (DNS);视频流;IP语音(VoIP)。
1.udp协议客户端允许发空
2.udp协议不会粘包
3.udp协议服务端不存在的情况下,客户端不会报错
4.udp协议支持并发
server端
import socket
# 创建套接字对象 UDP协议
s = socket.socket(type=socket.SOCK_DGRAM)
# 绑定ip+port
s.bind(('127.0.0.1', 8080))
# UDP 协议不需要双向通道, 所以不需要accept 直接进入通信循环
while True:
# 接收客户端信息和地址
data, addr = s.recvfrom(1024)
print('客户端发来的信息', data.decode('utf-8'))
print('客户端发来的地址', addr)
# 将客户端发来的信息大写后发给客户端
s.sendto(data.upper(), addr)
client端
import socket
# 创建套接字对象 修改为UDP协议
c = socket.socket(type=socket.SOCK_DGRAM)
# 服务端ip+port
s_addr = ('127.0.0.1', 8080)
# UDP协议通信部需要创建通道连接 直接进入通信循环
while True:
# 向指定的服务端发送信息
c.sendto(b'hello', s_addr)
# 接收服务端信息 解压赋值
msg, addr = c.recvfrom(1024)
print('服务端发来的数据', msg.decode('utf-8'))
print('服务端发来的地址', addr)
粘包问题
收发数据量不匹配造成
粘包现象分两种:
1.连续发送小的数据,间隔时间很短,有可能一次就接收到了这几个连续的拼接在一起的小数据.
2.当你一次接收的数据长度小于你一次发送的数据长度,那么一次接受完剩下的数据会在下一次接收数据的时候被一起接收
解决粘包问题:
解决方案一:
将要发送的字节流总大小让接收端知晓,然后接收端发一个确认消息给发送端,然后发送端再发送过来后面
的真实内容,接收端再来一个死循环接收完所有数据。
解决方案二:
通过struck模块将需要发送的内容的长度进行打包,打包成一个4字节长度的数据发送到接收端,再发送真实内
容,
接收端取出前4个字节,然后对这四个字节的数据进行解包,拿到发送的内容的长度
然后通过这个长度来循环继续接收我们实际要发送的内容
服务端
1.先制作一个发送给客户端的字典
2.制作字典的报头
3.发送字典的报头
4.发送字典
5.再发真实数据
客户端
1.先接受字典的报头
2.解析拿到字典的数据长度
3.接受字典
4.从字典中获取真实数据的长度
5.接受真实数据
struct模块
该模块可以把一个类型,如数字,转成固定长度的bytes
import struct
obj = struct.pack('i',123456)
print(len(obj)) # 4
obj = struct.pack('i',898898789)
print(len(obj)) # 4
# 无论数字多大,打包后长度恒为4
使用struct解决黏包
借助struct模块,我们知道长度数字可以被转换成一个标准大小的4字节数字。因此可以利用这个特点来预先发送数据长度。
server端
import socket
import subprocess
import struct
import json
server = socket.socket()
server.bind(('127.0.0.1',8080))
server.listen(5)
while True:
conn, addr = server.accept()
while True:
try:
cmd = conn.recv(1024)
if len(cmd) == 0:break
cmd = cmd.decode('utf-8')
obj = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
res = obj.stdout.read() + obj.stderr.read()
print(res) # res 是二进制格式
d = {'name':'jason','file_size':len(res),'info':'asdhjkshasdad'}
json_d = json.dumps(d) # 将字典序列化成字符串,便于编码传输
# 1.先制作一个字典的报头
header = struct.pack('i',len(json_d)) # 报头自动被编码成二进制
print(header) # >>> b'<\x00\x00\x00'
# 2.发送字典报头
conn.send(header)
# 3.发送字典
conn.send(json_d.encode('utf-8')) # 将字典编码发送
# 4.再发真实数据
conn.send(res)
# conn.send(obj.stdout.read())
# conn.send(obj.stderr.read())
except ConnectionResetError as e:
print(e)
break
conn.close()
client端
import socket
import struct
import json
client = socket.socket()
client.connect(('127.0.0.1',8080))
while True:
msg = input('>>>:').encode('utf-8')
if len(msg) == 0:continue
client.send(msg)
# 1.先接受字典报头
header_dict = client.recv(4) # 接收的报头是二进制格式
# 2.解析报头 获取字典的长度
dict_size = struct.unpack('i',header_dict)[0] # 解包的时候一定要加上索引0
print(dict_size)
# 3.接收字典数据
dict_bytes = client.recv(dict_size) # 按照字典的长度接收字典
dict_json = json.loads(dict_bytes.decode('utf-8')) # 将字典反序列化并解码出来
print(dict_json)
# 4.从字典中获取信息
recv_size = 0
real_data = b'' # 初始化二进制
while recv_size < dict_json.get('file_size'): # dict_json.get('file_size') = len(res)
data = client.recv(1024) # 接收1024个字节
real_data += data # 每读取一次二进制数据拼接一次
recv_size += len(data) # 每次读取的长度相加,当总长度和len(res)向同时,结束
print(real_data.decode('gbk')) # 将读取的二进制数据解码出来
socketserver 模块
server端
import socketserver
class MyServer(socketserver.BaseRequestHandler):
def handle(self):
# print('来啦 老弟')
while True:
data = self.request.recv(1024)
print(self.client_address) # 客户端地址
print(data.decode('utf-8'))
self.request.send(data.upper())
if __name__ == '__main__':
"""只要有客户端连接 会自动交给自定义类中的handle方法去处理"""
server = socketserver.ThreadingTCPServer(('127.0.0.1',8080),MyServer) # 创建一个基于TCP的对象
server.serve_forever() # 启动该服务对象
client端
import socket
client = socket.socket()
client.connect(('127.0.0.1',8080))
while True:
client.send(b'hello')
data = client.recv(1024)
print(data.decode('utf-8'))
Tcp案例:文件上传
client端
import socket
import os
import struct
import json
# 创建套接字对象
c = socket.socket() # 默认TCP协议
# 连接服务端ip+port
c.connect(('127.0.0.1', 8080))
# 通信循环
while True:
file_path = r'D:\OldBoy-py\网路编程\TCP协议\socket套接字\上传文件\file_movie'
file_list = os.listdir(file_path)
for index, movie in enumerate(file_list):
print(index+1, movie)
choice = input('请选择要上传的视频>>>:').strip()
if not choice.isdigit():
print('请输入数字')
choice = int(choice)
if not (choice > 0 and choice <= len(file_list)):
print('请选择正确编号')
movie_name = file_list[choice-1]
# 获得文件路径
movie_path = os.path.join(file_path, movie_name)
# 获得文件大小
movie_size = os.path.getsize(movie_path)
# 制作文件信息字典
d = {
'name': '电影',
'file_size': movie_size
}
# 序列化字典
d_json = json.dumps(d)
# 将序列化后的字典转为二进制类型
d_bytes = d_json.encode('utf-8')
# 制作报头
header = struct.pack('i', len(d_bytes))
# 向服务端发送报头
c.send(header)
# 向服务端发送字典
c.send(d_bytes)
# 发送真实数据 读取文件信息,循环发送
with open(movie_path, 'rb') as f:
for line in f:
# 发送
c.send(line) # rb 模式读出就是二进制格式
server端
import socket
import struct
import json
s = socket.socket()
# 绑定ip+port
s.bind(('127.0.0.1', 8080))
# 监听
s.listen(5)
# 连接循环
while True:
# 建立双向通道 获取通道地址
conn, addr = s.accept()
# 通信循环
while True:
try:
# 获取客户端信息(字典报头)
header_len = conn.recv(4)
# 解析报头 拆包 得到字典长度
d_len = struct.unpack('i', header_len)[0]
# 按字典长度接收字典
d_json = conn.recv(d_len)
# 解码字典
d_bytes = d_json.decode('utf-8')
# 把字典反序列化
d = json.loads(d_bytes)
# 获得真是数据长度
movie_size = d.get('file_size')
# 循环接收真实数据并写入文件
start_size = 0
with open(d.get('name'), 'wb') as f:
while start_size < movie_size:
# 按1024字节接收数据
data = conn.recv(1024)
# 写入文件
f.write(data)
start_size += len(data)
print('上传成功')
except ConnectionResetError as e:
print(e)
break
# 关闭通道
conn.close()
Udp案例: 简易版QQ
client端
import socket
c = socket.socket(type=socket.SOCK_DGRAM)
s_d = ('127.0.0.1', 8080)
while True:
msg = input('>>>')
c.sendto(msg.encode('utf-8'), s_d)
data, addr = c.recvfrom(1024)
print(data.decode('utf-8'))
server端
import socket
s = socket.socket(type=socket.SOCK_DGRAM)
s.bind(('127.0.0.1', 8080))
while True:
data, addr = s.recvfrom(1024)
print(data.decode('utf-8'))
msg = input('>>>:')
s.sendto(msg.encode('utf-8'), addr)