1,socket通信
2,socket对象的参数
socket families:网络层
socket.AF_INET# IPV4
socket.AF_INET6# IPV6
socket.AF_UNIX # unix本机进程间通信
socket types:传输层
socket.SOCK_STREAM #TCP
socket.SOCK_DGRAM #UDP
socket.SOCK_RAW #原始套接字,可以伪造IP头
socket.SOCK_RDM #UDP,保证传到,但不保证顺序
3,黏包
场景:
单次接收设置1024,但是实际数据大于1024,多于1024的内容会再下次接收到
服务器连续两次send,可能会被放进缓冲区形成黏包
解决方法:
计算发送内容大小,先把大小发送给接收方,发送完大小后服务器必须加阻塞确认,防止黏包
拿到内容大小后,后续如果还有多次send多问题,就可以改用精确接收内容大小防止黏包了,无需每次阻塞确认
特别注意中文str长度1,变成bytes后长度是3
4,模拟SSH
服务器端:
import socket
import os
server = socket.socket()
server.bind(('localhost', 19999)) # 绑定端口
server.listen(3) # 监听端口,这里不是并发
while True:
conn, addr = server.accept() # 等待连接,进入阻塞状态
while True:
cmd = conn.recv(1024) # 接收大小是1024
if not cmd: # linux会陷入recv死循环,建议都加上防止反复接收死循环
break
cmd = cmd.decode()
cmd_res = os.popen(cmd).read().encode()
cmd_lens = len(cmd_res)
conn.send(str(cmd_lens).encode())
client_ack = conn.recv(1024) # 加个确认防止黏包
conn.send(cmd_res)
# server.close() # 这里如果加close,server不会立即关闭,会在当前client结束后再关
客户端:
import socket
client = socket.socket()
client.connect(('localhost', 19999))
while True:
msg = input('>>: ').strip()
if not msg:
continue
elif msg == 'exit': # 退出循环,关闭连接
break
client.send(msg.encode()) # 只能send bytes,默认utf-8
cmd_res_size = client.recv(1024)
cmd_res_size = int(cmd_res_size.decode()) # bytes -> str -> int
client.send(b'ack') # 发送确认,解决长度与实际data之间的黏包问题
received_size = 0
received_data = b''
while received_size < cmd_res_size:
data = client.recv(1024)
received_size += len(data)
received_data += data
print(received_data.decode())
client.close()
5,socket接收文件
流程:读取文件名,检测文件是否存在,打开文件,检测文件大小,发送文件大小给客户端,等待客户端确认,边读边发,发送MD5
服务器端:
import socket
import os
import hashlib
server = socket.socket()
server.bind(('localhost', 19999))
server.listen()
while True:
conn, addr = server.accept()
while True:
data = conn.recv(1024)
if not data:
break
cmd, filename = data.decode().split()
if os.path.isfile(filename):
f = open(filename, 'rb') # 'rb'打开,后面不用encode了
file_size = os.stat(filename).st_size
conn.send(str(file_size).encode()) # 发送文件大小
conn.recv(1024) # 等待确认
m = hashlib.md5()
for line in f: # 发送文件,f是迭代器
m.update(line) # 逐行更新计算MD5
conn.send(line)
f.close()
conn.send(m.hexdigest().encode()) # 将MD5发送给客户端
客户端:
使用get + 文件名,获取文件
import socket
import hashlib
client = socket.socket()
client.connect(('localhost', 19999))
while True:
cmd = input('>>: ').strip()
if not cmd:
continue
elif cmd == 'break':
break
elif cmd.startswith('get'):
client.send(cmd.encode()) # 只能send bytes,默认utf-8
file_size = client.recv(1024)
file_size = int(file_size.decode()) # bytes -> str -> int
client.send(b'ack') # 发送确认,解决长度与实际data之间的黏包问题
received_size = 0
file_name = cmd.split()[1]
m = hashlib.md5()
f = open(file_name + '.new', 'wb')
while received_size < file_size:
size = 1024
if file_size - received_size > 1024:
buff = file_size - received_size # 最后一次收实际数据,防止黏包把服务器MD5写入文件
data = client.recv(size)
received_size += len(data)
m.update(data)
f.write(data)
f.close()
client_file_md5 = m.hexdigest()
server_file_md5 = client.recv(1024).decode()
print('客户端文件MD5:', client_file_md5)
print('服务器端文件MD5:', server_file_md5)
client.close()
6,socketserver
对socket进行二次封装,简化了socket服务器端编写,实现服务器端并发
socketserver实现模拟SSH服务器端(客户端不变):
class TCPHandler(socketserver.BaseRequestHandler):
def handle(self):
while True: # 不写while True,每个连接只能处理一次命令
cmd = self.request.recv(1024)
if not cmd: # cmd为空代表客户端断开了,不做非空判断客户端断开时可能会无限循环
break
cmd = cmd.decode()
cmd_res = os.popen(cmd).read().encode()
cmd_lens = len(cmd_res)
self.request.send(str(cmd_lens).encode())
self.request.recv(1024)
self.request.send(cmd_res)
if __name__ == '__main__':
server = socketserver.TCPServer(('localhost', 9999), TCPHandler)
server.serve_forever()
将TCPServer改成ThreadingTCPServer可以实现多并发