date: 2021-07-09title: python实现TCP及UDP连接 #标题
tags: #标签
categories: python # 分类
TCP
TCP协议实现简单聊天系统
server端代码
'''
服务端代码:
可以实时聊天,按Q退出,退出当前链接后等待下一个链接进来
'''
import socket
sk = socket.socket()
sk.bind(('127.0.0.1', 9000))
sk.listen()
while 1:
conn, addr = sk.accept()
while 1:
msg = input('>>> ')
conn.send(msg.encode('utf-8'))
if msg.upper() == 'Q': break
res = conn.recv(1024).decode('utf-8')
if res.upper() == 'Q': break
print(res)
conn.close()
sk.close()
client端代码
'''
客户端代码:
可以实时和服务端聊天,按Q退出当前程序
'''
import socket
sk = socket.socket()
sk.connect(('127.0.0.1', 9000))
while 1:
res = sk.recv(1024).decode('utf-8')
if res.upper() == 'Q': break
print(res)
msg = input('>>> ')
sk.send(msg.encode('utf-8'))
if msg.upper() == 'Q': break
sk.close()
粘包的现象及解决方案
什么是粘包?
只有TCP有粘包现象,UDP没有。
应用程序看到的数据是一个整体,或称为一个流(stream),而一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议。
而UDP协议是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据。
而消息可以认为发送方一次性write/send的数据为一个消息,但是,当send一条信息时,无论底层怎样分段分片,TCP协议层会把构成整条信息的数据段排序完成后才呈现在内核缓冲区。即面向流的通信是无消息保护边界的。
由于UDP支持的是一对多的模式,所以接收端的套接字缓冲区采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址、端口等信息),对于接受端来说就容易进行区分处理。即面向消息的通信是有消息保护边界的
所谓粘包问题主要是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要发送的数据都很少,通常TCP会根据优化算法(Nagle算法)把这些数据合成为一个TCP段后一次发送出去。
两种情况下会发生粘包
1.发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据量很小,会合并到一起,产生粘包)
2.接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一部分,服务端下一次接收的时候还是从缓冲区取上次遗留的数据,产生粘包)
解决粘包
通过struck模块将需要发送的内容的长度进行打包成一个4字节长度的数据发送给接收端,接收端只要取出前4个字节,然后对这个字节的数据进行解包,拿到要发送的内容的长度,通过这个长度来继续接收实际要发送的内容。
server端代码
import socket
import struct
def data_len(data):
'''
发送数据长度,struct.pack是将数据长度编码成一个4字节的数据
:param data:
:return:
'''
num = struct.pack('i', len(data))
conn.send(num)
sk = socket.socket()
sk.bind(('127.0.0.1', 9000))
sk.listen()
conn, clinet = sk.accept()
msg1 = input('>>> ').encode('utf-8')
msg2 = input('>>> ').encode('utf-8')
msg3 = input('>>> ').encode('utf-8')
data_len(msg1)
conn.send(msg1)
data_len(msg2)
conn.send(msg2)
data_len(msg3)
conn.send(msg3)
conn.close()
sk.close()
clinet代码
from time import sleep
import socket
import struct
def data_lenth():
'''
返回要接受的数据长度
struct.unpack是将接受的struct.pack处理后的数据反编译为真正的数据长度
:return:
'''
res = sk.recv(4)
return struct.unpack('i', res)[0] # unpak返回的是一个元组,第一个元素就是数据原本的长度
sk = socket.socket()
sk.connect(('127.0.0.1', 9000))
msg1 = sk.recv(data_lenth()).decode('utf-8')
print(msg1)
sleep(1)
msg2 = sk.recv(data_lenth()).decode('utf-8')
print(msg2)
sleep(1)
msg3 = sk.recv(data_lenth()).decode('utf-8')
print(msg3)
sk.close()
socketserver模块
socketserver模块简介
在python的socket编程中,使用socket模块的时候,是不能实现多个连接的,当然如果加入其它的模块是可以的,例如select模块,在这里简单的介绍下socketserver模块。
socketserver,看其名字,就知道是一个socket的服务器模块的使用,在这个模块中,主要也就是实现服务器类的相关功能,在其中,也就是将socket模块和select模块进行了封装,从而创建了一些基类供人使用。
socketserver框架是一个基本的socket服务器端框架, 使用了threading来处理多个客户端的连接, 使用seletor模块来处理高并发访问, 是值得一看的python 标准库的源码之一。
socket不支持多并发,socketserver最主要的作用:就是实现一个并发处理,前面只是铺垫。
socketserver就是对socket的再封装。简化网络服务器版的开发。
socketserver一共有这么几种类型:
- TCP 协议
class socketserver.TCPServer(server_address, RequestHandlerClass, bind_and_activate=True)
- UDP 协议
class socketserver.UDPServer(server_address, RequestHandlerClass, bind_and_activate=True)
- Unix 本机之间进程的TCP、UDP (不常用)
class socketserver.UnixStreamServer(server_address, RequestHandlerClass, bind_and_activate=True)
class socketserver.UnixDatagramServer(server_address, RequestHandlerClass,bind_and_activate=True)
创建一个socketserver 至少分以下几步:
- 首先,必须创建一个请求处理类,继承BaseRequestHandlerclass类并且重写父类的handle()方法,该方法将处理传入的请求。
- 其次,必须实例化一个上面类型中的一个类(如TCPServer)传递服务器的地址和你上面创建的请求处理类 给这个TCPServer。
- 然后,调用handle_request()或者serve_forever()方法来处理一个或多个请求。
server.handle_request() # 只处理一个请求,处理完就退出了
server.serve_forever() # 处理多个请求,永远执行。
最后,调用server_close()关闭socket(这步可以忽略,因为在实际中,我们是直接kill此服务来停止他的,并不是让程序自己退出)。
socketserver服务器端和客户端代码
服务端代码
import time
import socketserver
ip_port = ('127.0.0.1', 9000) # 定义服务端的监听地址
# 自定义类名Myserver,继承socketserver.BaseRequestHandler类
class Myserver(socketserver.BaseRequestHandler):
def setup(self): # setup方法可选,但名字是固定的,如果不写,则继承父类的,父类中的setup方法什么都没做
# 当链接进来后,会先执行setup方法中的代码
print("conn is :", self.request) # conn
print("addr is :", self.client_address) # addr
def handle(self): # 必写,名字也是固定的
conn = self.request # 相当于socket模块中建立链接后的conn
while True:
try: # try中的就是真正要执行的代码块
data = conn.recv(1024).decode('utf-8')
conn.send(data.upper().encode('utf-8'))
time.sleep(0.5)
except ConnectionResetError as e: # 定义异常捕获
break
if __name__ == '__main__':
server = socketserver.ThreadingTCPServer(ip_port, Myserver) # 实例化server对象,传入监听地址及上面定义的类
server.serve_forever() # 相当于循环调用监听服务,直至程序退出
客户端代码
import socket
ip_port=('127.0.0.1',9000)
sk = socket.socket()
sk.connect(ip_port)
while True:
sk.send(b'hello')
content = sk.recv(1024).decode('utf-8')
print(content)
UDP
UDP协议实现简单聊天系统
服务端代码
import socket
'''
type值默认为:SOCK_STREAM,表示TCP协议
SOCK_DGRAM表示UDP协议
'''
sk = socket.socket(type=socket.SOCK_DGRAM)
sk.bind(('127.0.0.1', 9000))
while 1:
res, client = sk.recvfrom(1024)
res = res.decode('utf-8')
if res.upper() == 'Q': break
print(res)
msg = input('>>> ')
sk.sendto(msg.encode('utf-8'), client)
if msg.upper() == 'Q': break
sk.close()
客户端代码
import socket
sk = socket.socket(type=socket.SOCK_DGRAM)
server = ('127.0.0.1', 9000)
while 1:
msg = input('>>> ')
sk.sendto(msg.encode('utf-8'), server)
if msg.upper() == 'Q': break
res = sk.recv(1024).decode('utf-8')
if res.upper() == 'Q': break
print(res)
sk.close()
练习题
实现简单的用户认证
# server端代码如下:
# -*- coding : utf-8 -*-
import socket
import struct
def res_lenth():
'''
返回要接受的数据长度
struct.unpack是将接受的struct.pack处理后的数据反编译为真正的数据长度
:return:
'''
res = conn.recv(4)
return struct.unpack('i', res)[0] # unpak返回的是一个元组,第一个元素就是数据原本的长度
def send_lenth(data):
'''
发送数据长度,struct.pack是将数据长度编码成一个4字节的数据
:param data:
:return:
'''
num = struct.pack('i', len(data))
conn.send(num)
# 已有的用户信息
user_list = {
'张三': '123.com',
'lisi': 'jianzhao87.',
'王五': 'jianzhao'
}
ip_port = ('127.0.0.1', 9000)
sk = socket.socket()
sk.bind(ip_port)
sk.listen()
while 1:
conn, client = sk.accept()
print(f'当前连接的客户端是:{client}')
username = conn.recv(res_lenth()).decode('utf-8')
if username.upper() == 'Q': break
passwd = conn.recv(res_lenth()).decode('utf-8')
if username in user_list and passwd == user_list[username]:
msg = f'''
登录成功!
欢迎{username}登录!
'''
else:
msg = '账号或密码错误...'
send_lenth(msg.encode('utf-8'))
conn.send(msg.encode('utf-8'))
conn.close()
# client代码如下:
# -*- coding : utf-8 -*-
import socket
import struct
def res_lenth():
'''
返回要接受的数据长度
struct.unpack是将接受的struct.pack处理后的数据反编译为真正的数据长度
:return:
'''
res = sk.recv(4)
return struct.unpack('i', res)[0] # unpak返回的是一个元组,第一个元素就是数据原本的长度
def send_lenth(data):
'''
发送数据长度,struct.pack是将数据长度编码成一个4字节的数据
:param data:
:return:
'''
num = struct.pack('i', len(data))
sk.send(num)
while 1:
ip_port = ('127.0.0.1', 9000)
sk = socket.socket()
sk.connect(ip_port)
username = input('请输入用户名(按q或Q退出程序):').strip()
if username.upper() == 'Q':
send_lenth(username)
sk.send(username.encode('utf-8'))
break
passwd = input('请输入密码:').strip()
username = username.encode('utf-8')
passwd = passwd.encode('utf-8')
send_lenth(username)
sk.send(username)
send_lenth(passwd)
sk.send(passwd)
num = res_lenth()
msg = sk.recv(num)
print(msg.decode('utf-8'))
实现文件上传
server端代码
import socket
import struct
import json
def res_lenth():
'''
返回要接受的数据长度
struct.unpack是将接受的struct.pack处理后的数据反编译为真正的数据长度
:return:
'''
res = conn.recv(4)
return struct.unpack('i', res)[0] # unpak返回的是一个元组,第一个元素就是数据原本的长度
def send_lenth(data):
'''
发送数据长度,struct.pack是将数据长度编码成一个4字节的数据
:param data:
:return:
'''
num = struct.pack('i', len(data))
conn.send(num)
ip_port = ('127.0.0.1', 9000)
sk = socket.socket()
sk.bind(ip_port)
sk.listen()
conn, addr = sk.accept()
dic = conn.recv(res_lenth()).decode('utf-8')
new_dic = json.loads(dic)
print(new_dic)
file_path = r'D:\\' + new_dic['filename']
with open(file_path, mode='wb') as f:
while new_dic['filesize'] > 0:
content = conn.recv(1024)
f.write(content)
new_dic['filesize'] -= len(content)
conn.close()
sk.close()
client端代码
import json
import socket
import struct
import os
def res_lenth():
'''
返回要接受的数据长度
struct.unpack是将接受的struct.pack处理后的数据反编译为真正的数据长度
:return:
'''
res = sk.recv(4)
return struct.unpack('i', res)[0] # unpak返回的是一个元组,第一个元素就是数据原本的长度
def send_lenth(data):
'''
发送数据长度,struct.pack是将数据长度编码成一个4字节的数据
:param data:
:return:
'''
num = struct.pack('i', len(data))
sk.send(num)
ip_port = ('127.0.0.1', 9000)
abs_path = r"F:\work\考试\cmq\cmq-docs-1-1\独立版文档\cmq私有化部署培训\7.22号上午.wmv"
filename = os.path.basename(abs_path)
filesize = os.path.getsize(abs_path)
sk = socket.socket()
sk.connect(ip_port)
file_info = {'filename': filename, 'filesize': filesize}
str_dic = json.dumps(file_info)
send_lenth(str_dic)
sk.send(str_dic.encode('utf-8'))
with open(abs_path, mode='rb') as f:
while filesize > 0:
content = f.read(1024)
filesize -= 1024
sk.send(content)
sk.close()
Ftp功能实现
1.0版本
支持单用户登录验证,但上传和下载的路径都是写死的,并且不支持遍历目录。
server端代码:
import socket
import struct
import json
import sys, os
User_Status = {'username': '', 'Status': 1}
ip_port = ('127.0.0.1', 9000)
sk = socket.socket()
sk.bind(ip_port)
sk.listen()
def send_data(conn, data):
str_data = json.dumps(data)
b_data = str_data.encode('utf-8')
num = struct.pack('i', len(b_data))
conn.send(num)
conn.send(b_data)
def rec_data(conn):
tup = conn.recv(4)
num = struct.unpack('i', tup)[0]
msg = conn.recv(num).decode('utf-8')
data = json.loads(msg)
return data
# 装饰器,用于验证登录状态,上传和下载均需要登录才可以进行
def auth(func):
def inner(*args, **kwargs):
while User_Status['Status']:
userinfo = rec_data(conn)
with open('userinfo.txt', encoding='utf-8') as f:
for line in f:
user, pwd = line.strip().split('|')
if user == userinfo['username'] and pwd == userinfo['passwd']:
res = True
User_Status['Status'] = 0
User_Status['username'] = user
break
else:
res = False
login_status = {'opt': 'login', 'status': res}
send_data(conn, login_status)
ret = func(*args, **kwargs)
return ret
return inner
@auth
def upload(conn):
DownLoad_Path = r"C:\Users\Administrator\Desktop\aaa\\"
File_info = rec_data(conn)
Abs_Path = DownLoad_Path + File_info['File_Name']
with open(Abs_Path, mode='wb') as f:
while File_info['File_Size'] > 0:
data = conn.recv(1024)
f.write(data)
File_info['File_Size'] -= len(data)
@auth
def download(conn):
Abs_Path = r"F:\work\腾云忆想\考试\cmq\cmq-docs-1-1\独立版文档\cmq私有化部署培训\7.22号上午.wmv"
File_Size = os.path.getsize(Abs_Path)
File_Name = os.path.basename(Abs_Path)
File_info = {'File_Name': File_Name, 'File_Size': File_Size}
send_data(conn, File_info)
with open(Abs_Path, mode='rb') as f:
while File_Size > 0:
data = f.read(1024)
conn.send(data)
File_Size -= 1024
def main():
opt_list = rec_data(conn)
while True:
num = rec_data(conn)
if num == 'Q': break
This_Modules = sys.modules[__name__]
getattr(This_Modules, opt_list[int(num) - 1])(conn)
conn, addr = sk.accept()
if __name__ == '__main__':
main()
conn.close()
sk.close()
client端代码:
import socket
import struct
import json
import sys, os
ip_port = ('127.0.0.1', 9000)
sk = socket.socket()
sk.connect(ip_port)
User_Status = {'username': '', 'Status': 1}
def send_data(sk, data):
str_data = json.dumps(data)
b_data = str_data.encode('utf-8')
num = struct.pack('i', len(b_data))
sk.send(num)
sk.send(b_data)
def rec_data(sk):
tup = sk.recv(4)
num = struct.unpack('i', tup)[0]
msg = sk.recv(num).decode('utf-8')
data = json.loads(msg)
return data
# 装饰器,用于验证登录状态,上传和下载均需要登录才可以进行
def auth(func):
def inner(*args, **kwargs):
while User_Status['Status']:
username = input('请输入用户名:')
passwd = input('请输入密码:')
dic = {'username': username, 'passwd': passwd}
send_data(sk, dic)
res = rec_data(sk)
if res['opt'] == 'login' and res['status']:
print('登录成功!')
User_Status['Status'] = 0
User_Status['username'] = username
break
else:
print('登录失败!')
ret = func(*args, **kwargs)
return ret
return inner
@auth
def download(sk):
DownLoad_Path = r"C:\Users\Administrator\Desktop\\"
File_info = rec_data(sk)
Abs_Path = DownLoad_Path + File_info['File_Name']
with open(Abs_Path, mode='wb') as f:
while File_info['File_Size'] > 0:
data = sk.recv(1024)
f.write(data)
File_info['File_Size'] -= len(data)
@auth
def upload(sk):
Abs_Path = r"C:\Users\Administrator\Desktop\uTools-1.3.5.exe"
File_Size = os.path.getsize(Abs_Path)
File_Name = os.path.basename(Abs_Path)
File_info = {'File_Name': File_Name, 'File_Size': File_Size}
send_data(sk, File_info)
with open(Abs_Path, mode='rb') as f:
while File_Size > 0:
data = f.read(1024)
sk.send(data)
File_Size -= 1024
def main():
opt_list = ['download', 'upload']
send_data(sk, opt_list)
while True:
for index, opt in enumerate(opt_list, 1):
print(index, opt)
num = input('请输入您要进行的操作(按q退出):').strip()
if num.upper() == 'Q':
send_data(sk, 'Q')
break
elif not num.isdecimal():
continue
elif int(num) - 1 < len(opt_list):
send_data(sk, num)
This_Modules = sys.modules[__name__]
getattr(This_Modules, opt_list[int(num) - 1])(sk)
else:
print('请输入有效的操作...')
if __name__ == '__main__':
main()
sk.close()