date: 2021-07-09title: python实现TCP及UDP连接 #标题
tags: #标签
categories: python # 分类

记录下python操作TCP/UDP网络连接。

TCP

TCP协议实现简单聊天系统

server端代码
  1. '''
  2. 服务端代码:
  3. 可以实时聊天,按Q退出,退出当前链接后等待下一个链接进来
  4. '''
  5. import socket
  6. sk = socket.socket()
  7. sk.bind(('127.0.0.1', 9000))
  8. sk.listen()
  9. while 1:
  10. conn, addr = sk.accept()
  11. while 1:
  12. msg = input('>>> ')
  13. conn.send(msg.encode('utf-8'))
  14. if msg.upper() == 'Q': break
  15. res = conn.recv(1024).decode('utf-8')
  16. if res.upper() == 'Q': break
  17. print(res)
  18. conn.close()
  19. 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没有。

python实现TCP及UDP连接 - 图1

应用程序看到的数据是一个整体,或称为一个流(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()