多线程的TCP协议服务端实现 socketserver(包含粘包详解)

多线程(multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。

一开始用socket来写客户端并不能实现多进程

from socket import *

server = socket(AF_INET,SOCK_STREAM)

server.bind(('127.0.0.1',8080))

server.listen(5)   #半进程池大小

while True:
    print("等待客户端接入")
    conn,client_addr = server.accept()
    print(conn,client_addr)


    while True:
        try:
            msg_from = conn.recv(1024).decode('utf-8')
            print(f"收到[{client_addr}]发来信息:[{msg_from}]")
            if len(msg_from) == 0:
                break
            conn.send(msg_from.upper().encode('utf-8'))
            print(f"返回信息[{msg_from.upper()}]")
        except Exception:
            break

    conn.close()

后来选择socketserver来实现多线程,服务端的代码比较固定,真正需要编辑的应该是数据收发部分。

先写下client的代码,由于是多线程,可以重复几个:

#!/usr/bin/env python 
# -*- coding:utf-8 -*-
# author: Frank  time:2023/1/12

from socket import *
import struct
import json

client = socket(AF_INET,SOCK_STREAM)

client.connect(('127.0.0.1',8081))


while True:
    cmd = input(">>>").strip()
    if len(cmd) == 0:continue
    if cmd == 'quit':break
    client.send(cmd.encode('utf-8'))

    # (*****)为了防止粘包:
    # 按照约定,前4位是头信息,那就先recv(4),然后反解出来完整信息,最终拿到body的size
    header_struct = client.recv(4)
    header_size = struct.unpack('i',header_struct)[0]    #解包返回的是一个元组,第一位是元数据的真实size
    header_json_byte = client.recv(header_size)
    header_json = header_json_byte.decode('gbk')
    header = json.loads(header_json)
    body_size = header['body_size']

    res = b''
    res_size = 0
    while res_size < body_size:
        res_from_server = client.recv(1024)
        res_size += len(res_from_server)
        res += res_from_server
    print(res.decode('gbk'))

client.close()



server代码:

#!/usr/bin/env python 
# -*- coding:utf-8 -*-
# author: Frank  time:2023/1/12


import subprocess
import socketserver
import json
import struct


class MyRequestHandler(socketserver.BaseRequestHandler):
    def handle(self):
        print(self.request)
        print(self.client_address)


        while True:
            try:   #防止客户端报错,服务端是需要一直online的
                cmd_from_client = self.request.recv(1024)
                if len(cmd_from_client) == 0 : break    #防止客户端突然中断,服务端一直接收空的情况
                cmd = cmd_from_client.decode('gbk')
                print("收到的命令是: %s"%cmd)
                res_obj = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
                # 打印正确输出的内容
                res_out = res_obj.stdout.read()   # 这边返回的是二进制
                # 打印错误返回的内容
                res_err = res_obj.stderr.read()
                res = res_out + res_err
                print("返回的结果是: %s"%res.decode('gbk'))

                # (*****)为了避免粘包:

                # 粘包问题: 服务端返回的数据超过了一次client.recv(数据大小) 中能收到的值,TCP是稳定的流式协议,没有recv到的数据
                #           也并不会丢掉,会存到内存中,会保存在客户端内存中,当再次执行recv的时候会收到之前没有收到的值

                # 解决办法: 核心是规定边界。我先发一个头文件过去,系统会把我的头文件和我后面要发的正文粘在一起(TCP内部逻辑),我
                #           采用一种方式,固定我头文件的字节数,跟客户端约定好,先读固定字节数,拿到我头文件的信息,文件中包含
                #           我正文的大小(len),这样客户端就好读取数据了

                header_dict = {
                    'filename' : 'a.txt',
                    'body_size': len(res),
                    'md5' : '234sdlfjklkwj423hj4jk3453'
                }
                header_json = json.dumps(header_dict)    #json用于信息传输格式 json字符串格式
                header_json_byte = header_json.encode('utf-8')

                # 用struct打包头,实际上是把头的长度信息包起来,用‘i’的方式,是固定长度为4的一个二进制字符串,
                # return出来是一个长度为4的二进制字符,这个二进制字符串代表的就是这个头的大小
                header_struct = struct.pack('i',len(header_json_byte))
                self.request.send(header_struct)   #先把头的大小发过去
                self.request.send(header_json_byte)  #再把头文件发过去,(里面是包含正文大小的信息的,除此之外也许还有别的待添加的信息)


                self.request.send(res)
            except Exception:
                break
        self.request.close()




# 服务端应该做两件事

# 第一件事:服务器应该循环从半连接池取连接,建立双向连接,并拿到对象conn
s = socketserver.ThreadingTCPServer(('127.0.0.1',8081),MyRequestHandler)
s.serve_forever()


#就等同于:
# while True:
#     conn,client_addr = server.accept()


# 第二件事:服务器应该循环去处理拿到的连接对象,与其进行通信循环 ===> 这边的原理是拿到一个连接之后,MyRequestHandler内部会去做处理
#          调用handle这个方法,所以这边通信循环的操作应该是写在handle里面
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值