Python基础 - 第八天 - Socket编程进阶

本篇内容:

1.解决socket粘包问题

2.通过socket传输大数据

3.socketserver的使用

一、解决socket粘包问题

1.粘包现象怎么出现的

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

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

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

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"))
时间: 2024-10-12 16:41:06

Python基础 - 第八天 - Socket编程进阶的相关文章

Python基础之(Socket编程)

一.什么是Socket Socket又称为套接字,它是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口.在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议.所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的. UNIX BSD发明了socket这种东

【爬坑】Python 3.6 在 Socket 编程时出现类型错误 TypeError: a bytes-like object is required, not &#39;str&#39;

1. 问题描述 Python 3.6 在 Socket 编程时出现错误如下 Traceback (most recent call last): File "F:/share/IdeaProjects/test/mypython/test/test10_tcpclient.py", line 17, in <module> sock.send(str) TypeError: a bytes-like object is required, not 'str' Process

Python(七)Socket编程、IO多路复用、SocketServer

本章内容: Socket IO多路复用(select) SocketServer 模块(ThreadingTCPServer源码剖析) Socket socket通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,应用程序通常通过"套接字"向网络发出请求或者应答网络请求. 功能: sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM,0) 参数一:地址簇 socket.AF_INET IPv4(默认)

VII Python(9)socket编程

socket编程: 网络基础: TCP/IP: socket模型: python socket C/S开发: 非阻塞(select.poll.epoll) 网络基础: OSI七层协议.TCP/IP分层 注: 物理层(数据之间比特流的传输.物理接口.电气特性:硬件接口标准,如RJ45水晶头.无线网络传输802.11b等): 数据链路层(组帧,进行硬件地址寻址,差错校验等功能:ARP.RARP:单个链路上数据的传输,如传输依赖的是光纤.双绞线.还是无线等方式): 网络层(进行逻辑地址寻址,实现不同网

python第七周-面向对象编程进阶

申明:本文内容主要转自Alex老师的博客,仅供学习记录! 静态方法 只是名义上归类管理,实际上在静态方法里访问不了类实例中的任何属性 通过@staticmethod装饰器即可把其装饰的方法变为一个静态方法,什么是静态方法呢?其实不难理解,普通的方法,可以在实例化后直接调用,并且在方法里可以通过self.调用实例变量或类变量,但静态方法是不可以访问实例变量或类变量的,一个不能访问实例变量和类变量的方法,其实相当于跟类本身已经没什么关系了,它与类唯一的关联就是需要通过类名来调用这个方法 1 clas

python网络编程之socket编程

一 客户端/服务器架构 即C/S架构,包括 1.硬件C/S架构(打印机) 2.软件C/S架构(web服务) 美好的愿望: 最常用的软件服务器是 Web 服务器.一台机器里放一些网页或 Web 应用程序,然后启动 服务.这样的服务器的任务就是接受客户的请求,把网页发给客户(如用户计算机上的浏览器),然 后等待下一个客户请求.这些服务启动后的目标就是"永远运行下去".虽然它们不可能实现这样的 目标,但只要没有关机或硬件出错等外力干扰,它们就能运行非常长的一段时间. 生活中的C/S架构: 商

python基础数据类型补充以及编码进阶

01 内容大纲 基础数据类型的补充 数据类型之间的转换 编码的进阶 02 具体内容: 数据类型的补充: str # str :补充的方法练习一遍就行. s1 = 'taiBAi' # capitalize 首字母大写,其余变小写 print(s1.capitalize()) # swapcase 大小写翻转 print(s1.swapcase()) # title 每个单词的首字母大写 msg= 'taibai say3hi' print(msg.title()) s1 = 'barry' #

python基础5-面向对象编程

1.类和对象之封装 #方法1 def Bar(): print "Bar" def Hello(name): print "i am %s" %(name) #方法2 class Foo(): def Bar(self): print 'Bar' def Hello(self,name): print "i am %s" %name 对于数据库的增删改查 ####方式1 def fetch(hostname,port,username,passw

Python入门:tcp socket编程

初学脚本语言python,测试可用的tcp通讯程序: 服务器: #!/usr/bin/env python # -*- coding: utf-8 -*- import socket import threading import time def tcplink(sock, addr): print('Accept new connection from %s:%s...' % addr); sock.send(b'Welcome!!!'); while True: data = sock.r