本篇内容:

1.解决socket粘包问题

2.通过socket传输大数据

3.socketserver的使用



一、解决socket粘包问题

1.粘包现象怎么出现的

粘包是通过socket传输数据时不可避免的问题,也是我们要注意的问题。

当上次发送的数据和本次发送的数据是通过一次发送动作发送出去的,这样就出现了粘包情况。

什么情况下会将两次发送操作合并成一次发送操作?在写代码时将两次send写在一起、紧挨着,中间没有其它动作的情况下,就会合并发送次数。如下图:

Python基础 - 第八天 - Socket编程进阶_第1张图片


2.解决粘包问题的方法

当两次发送的数据用途不一样,并且两次的数据是粘在一起发送给对端的,对端接收数据后就无法对数据做处理,这时我们就要解决粘包问题。

①一发一收的方式解决粘包问题

send发送数据后,紧接着再通过recv接收数据,这样就会强制清空缓冲区,将缓冲区中的数据全部发送出去后再接收数据。如下图:


②增加两次发送操作的间隔时间

增加两次send操作的间隔时间,这样就会导致上一次缓冲区超时,从而不会等下一条了。注意,两次send操作的间隔时间最少要有0.5秒;



二、通过socket传输大数据

通过socket不仅能传输容量小的数据,也能传递容量大的数据或文件。

下面是通过socket传输文件,并且验证文件一致性的例子:

服务端:

import socket
import os
import hashlib


# 声明socket类型,同时生成socket连接对象
# family默认是AF_INET,type默认是SOCK_STREAM,可以不用再写了
server = socket.socket()

server.bind(("0.0.0.0", 55555))  # 绑定ip地址和端口号

server.listen(5)  # 监听连接。和连接通信后最多可以挂起5个连接

while True:  # 服务端能接收多个连接
    conn, addr = server.accept()
    print("新连接", addr)

    while True:  # 能和任意的一个连接互相通信多次
        print("等待的新指令")
        cmd = conn.recv(1024)  # 接收数据,设置最大只能接收1K数据

        if not cmd:  # 当客户端断开后,会一直接收空数据,防止进入死循环
            print("客户端断开")
            break

        # 获取动作和文件名
        action, filename = cmd.decode(encoding="utf-8").split()
        print("客户端需要下载的文件是", filename)

        if os.path.isfile(filename):  # 文件存在
            file_size = os.stat(filename).st_size  # 获取文件大小
            conn.send(str(file_size).encode(encoding="utf-8"))  # 向客户端发送文件大小

            # 接收客户端的确认信息。服务端一发一收从而解决粘包问题
            receive_client_ack = conn.recv(1024)
            print("接收到客户端反馈的确认信息为", receive_client_ack.decode(encoding="utf-8"))

            m = hashlib.md5()  # 生成md5对象

            with open(filename, "rb") as f:
                for line in f:
                    conn.send(line)  # 按照每次只发送一整行内容的方式向客户端发送
                    m.update(line)  # 将文件每一行的内容添加到md5中,注意hashlib模块只能处理bytes类型的内容

            md5_value = m.hexdigest()  # 按照16进制格式显示
            print("服务端计算的文件md5值为", md5_value)

            conn.send(md5_value.encode(encoding="utf-8"))  # 将文件的md5值发送给客户端

        else:
            print("文件%s不存在" % filename)

        print("发送完成")


客户端:

import socket
import hashlib


# 声明socket类型,同时生成socket连接对象
# family默认是AF_INET,type默认是SOCK_STREAM,可以不用再写了
client = socket.socket()

client.connect(("localhost", 55555))  # 连接的ip地址和端口号

while True:  # 客户端能多次给服务端发送消息
    cmd = input("输入命令\n>>>").strip()

    if len(cmd) == 0:  # send不能发送空内容,如果用send发送空内容会卡住
        print("输入的命令为空")
        continue

    if cmd.startswith("get"):  # 解析命令的动作
        client.send(cmd.encode(encoding="utf-8"))  # 将动作和文件名发送给服务端

        server_response = client.recv(1024)  # 接收服务端发送的文件大小,设置最大只能接收1K数据
        file_size = server_response.decode(encoding="utf-8")  # 把文件大小从二进制转换成字符串
        print("服务端发送的文件大小为 %s" % file_size)

        # 客户端一收一发,为了解决粘包问题
        client.send("已经准备好接收文件了,可以开始发送了".encode(encoding="utf-8"))  # 给服务端发送确认信息

        file_total_size = int(file_size)  # 文件总大小,从字符串转换成整型
        receive_size = 0  # 累计接收到的内容大小
        filename = cmd.split()[1]  # 获取文件名

        m = hashlib.md5()  # 生成md5对象

        with open(filename + ".new", "wb") as f:
            while receive_size < file_total_size:  # 累计接收到的内容大小小于文件总大小就一直接收

                # 剩余的内容大小大于1024,代表需要接收的次数不止一次
                if file_total_size - receive_size > 1024:
                    size = 1024  # 接收大小为1024
                else:  # 剩余的内容大小小于1024,代表一次就可以接收完剩余数据
                    size = file_total_size - receive_size  # 接收大小为剩余数据的大小

                data = client.recv(size)  # 接收服务端发送的内容
                f.write(data)  # 写入文件中
                receive_size += len(data)  # 将每次接收到数据的长度累加起来
                m.update(data)  # 将接收到的所有内容添加到md5中,注意hashlib模块只能处理bytes类型的内容
            else:
                print("客户端文件接收完成,客户端接收到的文件大小为", receive_size)

                client_md5_value = m.hexdigest()  # 按照16进制格式显示
                print("客户端计算的文件md5值为", client_md5_value)

        # 这里两个recv是连在一起的,代表服务端就是两个send连在一起,这样就有可能出现粘包情况
        # 上面在接收文件时,在最后一次接收时,将接收大小设置为文件剩余内容的大小,这样就解决了粘包的问题
        server_md5_value = client.recv(1024)  # 接收服务端发送的文件md5值
        print("服务端计算的文件md5值为", server_md5_value.decode(encoding="utf-8"))

    else:
        print("命令动作出错,无法识别")



三、socketserver的使用

1.socketserver的类型

①TCPServer

使用因特网TCP协议,它提供了客户端和服务器之间连续的数据流

语法:

socketserver.TCPServer(server_address, RequestHandlerClass, bind_and_activate=True)


②UDPServer

使用数据报,这是离散的信息包,到达顺序可能不对或是在传输过程中出现故障或丢失。参数与TCPServer相同;

语法:

socketserver.UDPServer(server_address, RequestHandlerClass, bind_and_activate=True)


③UnixStreamServer和UnixDatagramServer

这些较不常用的类类似于TCP和UDP类,但是使用Unix域套接字,它们在非unix平台上是不可用的。参数与TCPServer相同;

语法:

socketserver.UnixStreamServer(server_address, RequestHandlerClass, bind_and_activate=True)

socketserver.UnixDatagramServer(server_address, RequestHandlerClass, bind_and_activate=True)


这几个类的继承关系是:

TCPServer继承了BaseServer;

UnixStreamServer和UDPServer继承了TCPServer;

UnixDatagramServer继承了UDPServer;


2.创建一个socketserver至少需要遵循以下几步(支持并发的类和不支持并发的类都需要遵循)

  1.创建一个请求处理类,这个类要继承socketserver.BaseRequestHandler类,并且还要重写父类中的handle()方法(socketserver.BaseRequestHandler类中的handle()方法是空的。跟客户端所有交互都是在handle()方法中完成的);


  2.必须实例化一个服务器类(TCPServer或UnixStreamServer或UDPServer或UnixDatagramServer或ForkingTCPServer或ForkingUDPServer或ThreadingTCPServer或ThreadingUDPServer),并且将server地址(是一个包含ip地址和端口号的元组,和socket的地址一样)和上面创建的请求处理类传递给这个服务器类;


  3.

  通过实例化生成的实例来调用handle_request() ,它只能处理一个连接的请求,处理完该连接请求后就会退出;(不管使用的是支持多并发的类,还是不支持多并发的类,它都只能处理一个连接,处理完该连接后就会退出)

  通过实例化生成的实例来调用serve_forever(),它可以处理多个连接的请求(服务端能接收多个客户端的连接,但如果使用的是不支持多并发的类的话,除正在交互的连接外,其它连接都被挂起),处理完连接的请求后不会退出,会永远执行着;


  4.通过实例化生成的实例来调用server_close()就关闭了socket;


3.socketserver的语法

  承放数据内容的变量 = self.request.recv(数据量大小):接收消息。数据量大小的默认单位是字节。

 

  self.request.send(二进制类型的消息):发送消息;

 

  self.client_address:是一个具有两个元素的小元组(host, port),self.client_address[0]代表客户端的ip地址,self.client_address[1]代表客户端使用的端口号;


  server_close():清理服务器端,关闭服务器端;


  request_queue_size:代表请求队列的大小。如果需要很长时间来处理单个请求,那么在服务器繁忙时接收到的任何请求都被放置到队列中。一旦队列满了,来自客户端的请求将获得“拒绝连接”错误。默认值通常是5,但它可以被子类覆盖。(相当于普通socket的object.listen(backlog)中的backlog参数)


  allow_reuse_address:服务器是否允许重用地址,也就是地址重用功能。这默认为false,并且可以在子类中设置以更改策略。(可以解决地址被占用的问题)


4.socketserver实例,不支持多并发

服务端能接收多个客户端的连接,但同时只能和一个客户端的连接进行通信交互,其它连接被挂起。每个客户端都能向服务端多次发送消息。

服务端:

使用的是socketserver模块

import socketserver


class MyTCPHandler(socketserver.BaseRequestHandler):
    """创建MyTCPHandler请求处理类,并且MyTCPHandler类继承了BaseRequestHandler类"""

    # 重写handle()方法
    def handle(self):
        """处理跟客户端交互的方法函数"""

        while True:  # 使服务端能和一个连接进行多次通信

            # 当客户端断开后,服务端不会出现一直接收空数据从而进入死循环状态
            # 当客户端断开后,服务端会抛出ConnectionResetError错误,所以这里要抓取异常
            try:

                # 接收客户端发送的消息
                # 这里是self,不再是conn了,代表每接收到一个客户端的请求,都会实例化MyTCPHandler类
                self.data = self.request.recv(1024).strip()

                print("{} 发的消息是: ".format(self.client_address[0]))
                print(self.data.decode(encoding="utf-8"))

                # 向客户端发送消息
                self.request.send(self.data.upper())

            except ConnectionResetError as e:
                print("错误信息为", e)
                break  # 和客户端断开连接


if __name__ == "__main__":

    # 定义连接服务端的ip地址和要访问服务端的服务端口
    HOST, PORT = "localhost", 55555

    # 实例化TCPServer类,并且在实例化时将地址、MyTCPHandler类当作参数传递进去
    server = socketserver.TCPServer((HOST, PORT), MyTCPHandler)

    server.serve_forever()  # 可以处理多个请求,会永远执行着


客户端:

使用的是socket模块

import socket


# 声明socket类型,同时生成socket连接对象
# family默认是AF_INET,type默认是SOCK_STREAM,可以不用再写了
client = socket.socket()

client.connect(("localhost", 55555))  # 连接服务端的ip地址和要访问服务端的服务端口

while True:  # 客户端能多次给服务端发送消息

    message = input("输入你想要发送的消息\n>>>").strip()

    if len(message) == 0:  # send不能发送空内容,如果用send发送空内容会卡住
        print("输入的命令为空")
        continue

    # socket只能发送二进制类型的内容
    client.send(message.encode(encoding="utf-8"))  # 只能发送bytes类型,比特流的bytes类型

    data = client.recv(1024)  # 接收服务器端响应的数据,设置最多可以接收1K的数据,单位默认为字节
    print("服务器端响应的数据为: %s" % data.decode(encoding="utf-8"))


5.通过socketserver实现多并发

①让socketserver并发起来,必须要使用以下的一个多并发的类:

  socketserver.ForkingTCPServer

  socketserver.ForkingUDPServer

  socketserver.ThreadingTCPServer

  socketserver.ThreadingUDPServer


②修改方法

把下面这句代码
  server = socketserver.TCPServer((HOST, PORT), 创建的请求处理类的类名)
修改成下面的代码,就可以多并发了
  server = socketserver.ThreadingTCPServer((HOST, PORT), 创建的请求处理类的类名)
  server = socketserver.ThreadingUDPServer((HOST, PORT), 创建的请求处理类的类名)
  server = socketserver.ForkingTCPServer((HOST, PORT), 创建的请求处理类的类名)
  server = socketserver.ForkingUDPServer((HOST, PORT), 创建的请求处理类的类名)

只用修改类,实例化时要求传递的参数都是一样的;


③进程和线程的详解

  ● Threading:线程(生成一个线程的开销非常小,推荐使用)

server = socketserver.ThreadingTCPServer((HOST, PORT), 创建的请求处理类的类名)
server = socketserver.ThreadingUDPServer((HOST, PORT), 创建的请求处理类的类名)

客户端每连进一个连接,服务器端就会分配一个新的线程来处理这个客户端的请求;

 

  ● Forking:进程(生成一个进程的开销非常大)

server = socketserver.ForkingTCPServer((HOST, PORT), 创建的请求处理类的类名)
server = socketserver.ForkingUDPServer((HOST, PORT), 创建的请求处理类的类名)

客户端每连进一个连接,服务器端就会分配一个新的进程来处理这个客户端的请求;

注意,在windows上使用Forking会出现问题,要在linux上使用Forking;


④多并发的例子

服务端:

import socketserver


class MyTCPHandler(socketserver.BaseRequestHandler):
    """创建MyTCPHandler请求处理类,并且MyTCPHandler类继承了BaseRequestHandler类"""

    # 重写handle()方法
    def handle(self):
        """处理跟客户端交互的方法函数"""

        while True:  # 使服务端能和一个连接进行多次通信

            # 当客户端断开后,服务端不会出现一直接收空数据从而进入死循环状态
            # 当客户端断开后,服务端会抛出ConnectionResetError错误
            # 所以这里要抓取异常
            try:

                # 接收客户端发送的消息
                # 这里是self,不再是conn了,代表每接收到一个客户端的请求,都会实例化MyTCPHandler类
                self.data = self.request.recv(1024).strip()

                print("{} 发的消息是: ".format(self.client_address[0]))
                print(self.data.decode(encoding="utf-8"))

                # 向客户端发送消息
                self.request.send(self.data.upper())

            except ConnectionResetError as e:
                print("错误信息为", e)
                break  # 和客户端断开连接


if __name__ == "__main__":

    # 定义连接服务端的ip地址和要访问服务端的服务端口
    HOST, PORT = "localhost", 55555

    # 实例化ThreadingTCPServer类,支持多并发,并且在实例化时将地址、MyTCPHandler类当作参数传递进去
    server = socketserver.ThreadingTCPServer((HOST, PORT), MyTCPHandler)

    server.serve_forever()  # 可以处理多个请求,会永远执行着
    # server.handle_request()  # 只能处理一个连接的请求,处理完该连接请求后就会退出


客户端:

import socket


# 声明socket类型,同时生成socket连接对象
# family默认是AF_INET,type默认是SOCK_STREAM,可以不用再写了
client = socket.socket()

client.connect(("localhost", 55555))  # 连接服务端的ip地址和要访问服务端的服务端口

while True:  # 客户端能多次给服务端发送消息

    message = input("输入你想要发送的消息\n>>>").strip()

    if len(message) == 0:  # send不能发送空内容,如果用send发送空内容会卡住
        print("输入的命令为空")
        continue

    client.send(message.encode(encoding="utf-8"))  # 只能发送bytes类型,比特流的bytes类型

    data = client.recv(1024)  # 接收服务器端响应的数据,设置最多可以接收1K的数据,单位默认为字节
    print("服务器端响应的数据为: %s" % data.decode(encoding="utf-8"))