zoukankan      html  css  js  c++  java
  • 基于socket套接字的网络通讯

    Socket工作原理

      Socket是一个抽象层:

    先来理解什么是Socket:

    Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

    所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。

    也有人将socket说成ip+port,ip是用来标识互联网中的一台主机的位置,而port是用来标识这台机器上的一个应用程序,ip地址是配置到网卡上的,而port是应用程序开启的,ip与port的绑定就标识了互联网中独一无二的一个应用程序
    
    而程序的pid是同一台机器上不同进程或者线程的标识
    扫盲

    套接字的工作流程

    一个生活中的场景。你要打电话给一个朋友,先拨号,朋友听到电话铃声后提起电话,这时你和你的朋友就建立起了连接,就可以讲话了。等交流结束,挂断电话结束此次交谈。 生活中的场景就解释了这工作原理。

    先从服务器端说起。服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束

     简单的套接字通讯

    import socket
    
    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)        ##AF_INET = internet,SOCK_STREAM表示流式协议,也就是TCP
    phone.bind(('127.0.0.1',6609))          ##将服务端绑定到本机IP和固定端口,#ip应该是服务端这个软件运行那台机器的ip地址,port(0-65535)
    phone.listen(5)                         ##半连接池:控制的是同一时刻的链接请求数
    connect,ip_addr = phone.accept()        ##(connect=套接字对象,ip_addr=存放有客户端的ip和端口的元组)
    
    data = connect.recv(1024)               ##recv表示接收客户端发送的消息,1024单位是bytes,代表最大接收1024bytes
    print('收到客户端的信息: ',data)         ##打印客户端发送的信息
    connect.send(data.upper())              ##将客户端发来的信息转为大写发送给客户端
    
    connect.close()                         ##关闭本次通讯
    phone.close()                           ##关闭本socket
    服务端
    import socket           ##导入socket模块
    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)        ##AF_INET = internet,SOCK_STREAM表示流式协议,也就是TCP
    phone.connect(('127.0.0.1',6609))             ##链接服务器IP和端口
    user_inp = input('>>> ').strip()
    phone.send(user_inp.encode('utf-8'))            ##发送用户输入的信息
    data = phone.recv(1024)                         ##接收服务端返回信息,1024代表本次最大接收1024bytes内容
    print('收到服务端信息: ',data)                 ##打印接收到的内容
    客户端

    弊端:接收到一个客户端的指令答复后程序结束,之间的相互聊天通讯并不能实现循环

    用户套接字通讯循环

    import socket
    
    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)        ##AF_INET = internet,SOCK_STREAM表示流式协议,也就是TCP
    phone.bind(('127.0.0.1',6609))          ##将服务端绑定到本机IP和固定端口,#ip应该是服务端这个软件运行那台机器的ip地址,port(0-65535)
    phone.listen(5)                         ##半连接池:控制的是同一时刻的链接请求数
    connect,ip_addr = phone.accept()        ##(connect=套接字对象,ip_addr=存放有客户端的ip和端口的元组)
    while True:
        data = connect.recv(1024)               ##recv表示接收客户端发送的消息,1024单位是bytes,代表最大接收1024bytes
        print('收到客户端的信息: ',data)         ##打印客户端发送的信息
        connect.send(data.upper())              ##将客户端发来的信息转为大写发送给客户端
    
    connect.close()                         ##关闭本次通讯
    phone.close()                           ##关闭本socket
    通讯循环-服务端
    import socket           ##导入socket模块
    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)        ##AF_INET = internet,SOCK_STREAM表示流式协议,也就是TCP
    phone.connect(('127.0.0.1',6609))             ##链接服务器IP和端口
    while True:
        user_inp = input('>>> ').strip()
        phone.send(user_inp.encode('utf-8'))  ##发送用户输入的信息
        data = phone.recv(1024)  ##接收服务端返回信息,1024代表本次最大接收1024bytes内容
        print('收到服务端信息: ', data)  ##打印接收到的内容
    通讯循环-客户端

    弊端:当客户端主动关闭或强行关闭后,服务端也会随机崩溃,不同的系统表现不一样

      Windows:  抛出ConnectionResetError的异常,服务端崩溃

                           

      Unix:  unix下一直无限接收打印空消息,但是服务端不会崩溃

          

    针对上面的情况,对服务端添加异常捕捉,使其服务端不会崩溃和接收空消息

    import socket
    
    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)        ##AF_INET = internet,SOCK_STREAM表示流式协议,也就是TCP
    phone.bind(('127.0.0.1',6609))          ##将服务端绑定到本机IP和固定端口,#ip应该是服务端这个软件运行那台机器的ip地址,port(0-65535)
    phone.listen(5)                         ##半连接池:控制的是同一时刻的链接请求数
    connect,ip_addr = phone.accept()        ##(connect=套接字对象,ip_addr=存放有客户端的ip和端口的元组)
    while True:
        try:
            data = connect.recv(1024)               ##recv表示接收客户端发送的消息,1024单位是bytes,代表最大接收1024bytes
            if len(data) == 0:break                 ##针对linux或者mac
            print('收到客户端的信息: ',data)         ##打印客户端发送的信息
            connect.send(data.upper())              ##将客户端发来的信息转为大写发送给客户端
        except ConnectionResetError:                ##针对Windows下的优化
            break
    
    connect.close()                         ##关闭本次通讯
    phone.close()                           ##关闭本socket
    ConnectionResetError和空消息解决

    解决了上面的问题,发现还是有问题:

      1、不能同时给多个客户端提供服务

      2、客户端终止服务端依然会终止

    实现连接循环+通讯循环

    import socket
    
    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)        ##AF_INET = internet,SOCK_STREAM表示流式协议,也就是TCP
    phone.bind(('127.0.0.1',6609))          ##将服务端绑定到本机IP和固定端口,#ip应该是服务端这个软件运行那台机器的ip地址,port(0-65535)
    phone.listen(5)                         ##半连接池:控制的是同一时刻的链接请求数
    while True:
        connect,ip_addr = phone.accept()        ##(connect=套接字对象,ip_addr=存放有客户端的ip和端口的元组)
        while True:
            try:
                data = connect.recv(1024)               ##recv表示接收客户端发送的消息,1024单位是bytes,代表最大接收1024bytes
                if len(data) == 0:break                 ##针对linux或者mac
                print('收到客户端的信息: ',data)         ##打印客户端发送的信息
                connect.send(data.upper())              ##将客户端发来的信息转为大写发送给客户端
            except ConnectionResetError:                ##针对Windows下的优化
                break
    
        connect.close()                         ##关闭本次通讯
    phone.close()                           ##关闭本socket
    连接循环+通讯循环服务端
    import socket           ##导入socket模块
    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)        ##AF_INET = internet,SOCK_STREAM表示流式协议,也就是TCP
    phone.connect(('127.0.0.1',6609))             ##链接服务器IP和端口
    while True:
        user_inp = input('>>> ').strip()
        phone.send(user_inp.encode('utf-8'))  ##发送用户输入的信息
        data = phone.recv(1024)  ##接收服务端返回信息,1024代表本次最大接收1024bytes内容
        print('收到服务端信息: ', data)  ##打印接收到的内容
    连接循环+通讯循环客户端1
    import socket           ##导入socket模块
    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)        ##AF_INET = internet,SOCK_STREAM表示流式协议,也就是TCP
    phone.connect(('127.0.0.1',6609))             ##链接服务器IP和端口
    while True:
        user_inp = input('>>> ').strip()
        phone.send(user_inp.encode('utf-8'))  ##发送用户输入的信息
        data = phone.recv(1024)  ##接收服务端返回信息,1024代表本次最大接收1024bytes内容
        print('收到服务端信息: ', data)  ##打印接收到的内容
    连接循环+通讯循环客户端2

    测试方法:服务端开启后,分别运行客户单1和客户端2,实现服务端先对

           客户端1发送信息立刻提供响应,客户端2发送信息没有响应阻塞在原地,当客户端1关闭后客户端2立刻被响应,如果请求客户端sync阻塞数目大于半连接池,Windows将会被直接拒绝,而Linux并不会被拒绝

    为什么客户端2不能及时响应呢?

        

        我们很清晰的看到代码里面有两个While true,phone.accept()负责接收客户端的建立连接请求,而第二个后面实现了通讯循环,也就是说当第一个客户端接入的时候不出意外的话

        会直接进入到第二个while true,进行通讯循环,而后续接入的客户端全部被放入了半连接池,全部阻塞在phone.accept()这里,只有当第一个客户端结束了程序才有可能触发break

        从而回过头将客户端2的请求,从半链接池中提出出来,进入第二个while true来处理第二个客户端的请求,另外我的listen(5)半连接池设置为5,表示如果同时有7个客户端访问服务端,

        那么服务端将client1提供正常响应,client2-6这五个请求放入半连接池,一直阻塞,而client7则直接被拒绝,不能提供服务,当client1结束,client2则立刻被响应,client2结束,client3

        立刻被响应

    结论:  

      链接循环+通讯循环,客户端主动关闭,服务端也不会关闭

      这种同时服务由于局限于单进程,并不能实现真正意义上的同时响应和服务,关于这个问题以后再去完善它   

      

    制作一个简易的SSH命令执行工具

    import socket
    import subprocess
    
    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)        ##AF_INET = internet,SOCK_STREAM表示流式协议,也就是TCP
    phone.bind(('127.0.0.1',6609))          ##将服务端绑定到本机IP和固定端口,#ip应该是服务端这个软件运行那台机器的ip地址,port(0-65535)
    phone.listen(5)                         ##半连接池:控制的是同一时刻的链接请求数
    while True:
        connect,ip_addr = phone.accept()        ##(connect=套接字对象,ip_addr=存放有客户端的ip和端口的元组)
        while True:
            try:
                data = connect.recv(1024)               ##recv表示接收客户端发送的消息,1024单位是bytes,代表最大接收1024bytes
                if len(data) == 0:break                 ##针对linux或者mac
                print('收到客户端的信息: ',data)         ##打印客户端发送的信息
                obj = subprocess.Popen(
                    data.decode('utf-8'),
                    shell=True,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE
                )
                stdout=obj.stdout.read()
                stderr=obj.stderr.read()
                connect.send(stdout+stderr)                    ##将客户端发来的命令执行的到的正确输出返回
                # connect.send(stderr)                    ##将客户端发来的命令执行的到的错误输出返回
            except ConnectionResetError:                ##针对Windows下的优化
                break
    
        connect.close()                         ##关闭本次通讯
    phone.close()                           ##关闭本socket
    SSH服务端
    import socket           ##导入socket模块
    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)        ##AF_INET = internet,SOCK_STREAM表示流式协议,也就是TCP
    phone.connect(('127.0.0.1',6609))             ##链接服务器IP和端口
    while True:
        user_inp = input('>>> ').strip()
        phone.send(user_inp.encode('utf-8'))  ##发送用户输入的信息
        data = phone.recv(1024)  ##接收服务端返回信息,1024代表本次最大接收1024bytes内容
        print('收到服务端信息: ', data.decode('utf-8'))  ##打印接收到的内容
    SSH客户端

    弊端: 如果打印ls等简单的命令没什么问题,但是Linux下打ps -ef,就会发现一次只能接收我们客户端设置的1024个bytes,而你再打印ls发现打印的是上衣没有打印完的内容,也就是ls和上次为打印干净的ps -ef混在了一起,而这种现象我们称作粘包

    粘包问题

    发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。
    
    例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束
    
    所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
    
    此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。
    
    TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
    UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
    tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头,实验略
    udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠
    
    tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。
    
    两种情况下会发生粘包。
    
    发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)
    接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包) 
    粘包扫盲

    为什么会粘包?

        粘包问题只会出现在TCP中,UDP不会出现

        根本问题在于接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据

    为什么TCP是可靠的?UDP是不可靠的?

      tcp在数据传输时,发送端先把数据发送到自己的缓存中,然后协议控制将缓存中的数据发往对端,对端返回一个ack=1,发送端则清理缓存中的数据,对端返回ack=0,则重新发送数据,所以tcp是可靠的

    解决方案一:

    既然我的客户端设置最多接收1024,那我给它调大解决

    import socket           ##导入socket模块
    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)        ##AF_INET = internet,SOCK_STREAM表示流式协议,也就是TCP
    phone.connect(('127.0.0.1',6609))             ##链接服务器IP和端口
    while True:
        user_inp = input('>>> ').strip()
        phone.send(user_inp.encode('utf-8'))  ##发送用户输入的信息
        data = phone.recv(1024000)  ##接收服务端返回信息,1024代表本次最大接收1024bytes内容
        print('收到服务端信息: ', data.decode('utf-8'))  ##打印接收到的内容
    放大recv值-客户端修改

    结论:虽然解决了这种粘包问题,但是没有根本解决问题,因为你根本不知道下次可能会有更大的数据量,依然无法满足过大消耗的是服务器本身的内存,所以最好的办法就是设置1024,然后循环去取,直到取干净本次的内容,然后一次性打印;所以recv调整不可取

    解决方案二:

    既然遵循尼桑算法(数据量小,时间间隔短,打包黏在一起发送,来减少IO),那么延长发送时间

    
    

    解决方案三:

    根本问题在于客户端不知道自己发出的指令得出的结果有多大,所以导致客户端的1024太小,那么我们能不能在客户端发出查询指令后,服务端查出结果后先提前告诉客户端这个结果有多大呢?看代码

    import socket
    import subprocess
    import struct           ##用于将int转换为固定长度的bytes的模块,还支持很多
    
    
    
    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)        ##AF_INET = internet,SOCK_STREAM表示流式协议,也就是TCP
    phone.bind(('127.0.0.1',6609))          ##将服务端绑定到本机IP和固定端口,#ip应该是服务端这个软件运行那台机器的ip地址,port(0-65535)
    phone.listen(5)                         ##半连接池:控制的是同一时刻的链接请求数
    while True:
        connect,ip_addr = phone.accept()        ##(connect=套接字对象,ip_addr=存放有客户端的ip和端口的元组)
        while True:
            try:
                data = connect.recv(1024)               ##recv表示接收客户端发送的消息,1024单位是bytes,代表最大接收1024bytes
                if len(data) == 0:break                 ##针对linux或者mac
                print('收到客户端的信息: ',data)         ##打印客户端发送的信息
                obj = subprocess.Popen(
                    data.decode('utf-8'),
                    shell=True,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE
                )
                stdout=obj.stdout.read()
                stderr=obj.stderr.read()
                # 1、提前发送长度给客户端
                connect.send(struct.pack('i',len(stderr+stdout)))       ##将总长度转换为bytes提前发送给客户端,实际上是将int转换为bytes,而这个bytes的长度是4,是固定的
                # 2、发送数据
                connect.send(stdout+stderr)                    ##将客户端发来的命令执行的到的正确输出返回
                # connect.send(stderr)                    ##将客户端发来的命令执行的到的错误输出返回
            except ConnectionResetError:                ##针对Windows下的优化
                break
    
        connect.close()                         ##关闭本次通讯
    phone.close()                           ##关闭本socket
    自定义协议-固定长度报头-服务端
    import socket           ##导入socket模块
    import struct           ##用于将int转换为固定长度的bytes的模块,还支持很多
    
    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)        ##AF_INET = internet,SOCK_STREAM表示流式协议,也就是TCP
    phone.connect(('127.0.0.1',6609))             ##链接服务器IP和端口
    while True:
        user_inp = input('>>> ').strip()
        if len(user_inp) == 0:continue
        phone.send(user_inp.encode('utf-8'))  ##发送用户输入的信息
    
    
        # 1、首先接收服务端下次传输的长度
        header = phone.recv(4)                  ##客户端先接收4个bytes
        total_size = struct.unpack('i',header)[0]       ##拿到4个bytes后,通过struct.unpack解出元组得到下次接收的总长度
        recv_size = 0                       ##用于记录一共接收了多少bytes
        res = b''
        # 2、循环取值
        while recv_size < total_size:
            recv_data = phone.recv(1024)        ##接收新数据
            res += recv_data                    ##把每次的接收数据拼接起来给res
            recv_size += len(recv_data)         ##记录每次循环实时一共接收了多少bytes
    
        print('收到服务端信息: ', res.decode('utf-8'))  ##打印接收到的内容
    phone.close()
    自定义协议-固定长度报头-客户端

    结论: 解决了包黏在一起的问题

    弊端:目前只是查询命令,所以可能暂时够用,如果你查询的结果是102400000000000000000000000000,struct可能就不能满足了,那么这种场景存在吗?比如FTP上传10G的电影,可能就尬了,所以不可取

    解决方案四:

    看完上面三个,我们现在只需要集中思路想一想看看怎么解决大文件传输的问题,说白了看看怎么标记102400000000000000000000000000这么大的数字

    import socket
    import subprocess
    import struct           ##用于将int转换为固定长度的bytes的模块,还支持很多
    import json
    
    
    
    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)        ##AF_INET = internet,SOCK_STREAM表示流式协议,也就是TCP
    phone.bind(('127.0.0.1',6609))          ##将服务端绑定到本机IP和固定端口,#ip应该是服务端这个软件运行那台机器的ip地址,port(0-65535)
    phone.listen(5)                         ##半连接池:控制的是同一时刻的链接请求数
    while True:
        connect,ip_addr = phone.accept()        ##(connect=套接字对象,ip_addr=存放有客户端的ip和端口的元组)
        while True:
            try:
                data = connect.recv(1024)               ##recv表示接收客户端发送的消息,1024单位是bytes,代表最大接收1024bytes
                if len(data) == 0:break                 ##针对linux或者mac
                print('收到客户端的信息: ',data)         ##打印客户端发送的信息
                obj = subprocess.Popen(
                    data.decode('utf-8'),
                    shell=True,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE
                )
                stdout=obj.stdout.read()
                stderr=obj.stderr.read()
    
                # 1、制作报头
                header_dic = {
                    'filename':'xxxxx',
                    'total_size':len(stderr+stdout),        ##此处是传输数据的总大小
                    'md5':'xxxxxxxxxxxxxxxxxxxx',
                }
                header_str = json.dumps(header_dic)            ##为了传输到客户端后能得到字典,使用json,得到header的str类型
                heaer_bytes = header_str.encode('utf-8')        ##解码得到header的bytes类型,而现在的bytes长度是非常小了
    
                # 2、发送报头(header_bytes)长度
                connect.send(struct.pack('i',len(heaer_bytes)))      ##这里发送的是将header_dic转换为bytes后的长度的元组,但是客户端接收4bytes即可得到
    
                # 3、发送报头(header_bytes)
                connect.send(heaer_bytes)
    
                # 4、发送数据
                connect.send(stdout+stderr)
    
            except ConnectionResetError:                ##针对Windows下的优化
                break
    
        connect.close()                         ##关闭本次通讯
    phone.close()                           ##关闭本socket
    自定义报文-终极版-服务端
    import socket           ##导入socket模块
    import struct           ##用于将int转换为固定长度的bytes的模块,还支持很多
    import json
    
    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)        ##AF_INET = internet,SOCK_STREAM表示流式协议,也就是TCP
    phone.connect(('127.0.0.1',6609))             ##链接服务器IP和端口
    while True:
        user_inp = input('>>> ').strip()
        if len(user_inp) == 0:continue
        phone.send(user_inp.encode('utf-8'))  ##发送用户输入的信息
    
        # 1、接收报文的长度
        header_size = struct.unpack('i',phone.recv(4))[0]       #从元祖中拿到报文的长度
    
        # 2、接收报文
        header_bytes = phone.recv(header_size)                  #指定上面得到的报文长度接收json的bytes内容
        header_json = header_bytes.decode('utf-8')              #得到json的bytes内容后转成json文件
        header_dic = json.loads(header_json)                    #得到字典
        # 3、提取总大小
        total_size = header_dic['total_size']                   #根据字典取出查询结果的总大小
    
        recv_size = 0                       ##用于记录一共接收了多少bytes
        res = b''
        # 2、循环取值
        while recv_size < total_size:
            recv_data = phone.recv(1024)        ##接收新数据
            res += recv_data                    ##把每次的接收数据拼接起来给res
            recv_size += len(recv_data)         ##记录每次循环实时一共接收了多少bytes
    
        print('收到服务端信息: ', res.decode('utf-8'))  ##打印接收到的内容
    phone.close()
    自定义报文-终极版-客户端

    结论:我们通过这个方法实现了真正的粘包解决方案,而这种的重点在哪呢?重点在于之前struct直接int转bytes,会受int大小的限制,而通过先将int大小添加到字典在转成字符串就解决了这个问题

        方法一:  10000    表达方式    10000    直接int表达

        方法二:  10000    表达方式    5       使用dic表达,再转str

       很显然对于struct来说第二种方法更方便一些,能表达的值更大一些 

    socket并发实现

    socket并发实现前,首先要明白为什么上面的方案无法实现并发,是因为实现通讯为两个步骤一个是“循环建立连接”,一个是“循环通讯”,但是一个请求的完整流程意味着进入通讯阶段,就无法在建立连接,只有等本次通讯结束,那么

    socket的并发就是通过多线程技术来实现“循环建立连接”和“循环通讯”分离开来,来真正实现各自的循环

    import socketserver
    ##之所以无法实现并发是因为TCP有两种活一个是链接循环和通讯循环,把他们分开就好了
    
    
    # 通讯循环
    class MyTCPHandler(socketserver.BaseRequestHandler):
        def handle(self):
            print(self.request)     #self.request == conn
            while True:
                try:
                    data = self.request.recv(1024)
                    if not data:break
                    print('收到客户端消息: %s' %data)
                    self.request.send(data.upper())
                except ConnectionResetError:
                    break
            self.request.close()
    
    
    # 链接循环
    if __name__ == '__main__':
        ## 造线程
        server = socketserver.ThreadingTCPServer(('127.0.0.1',8081),MyTCPHandler)
        server.serve_forever()          ##永远循环,接请求
    socket并发编程-服务端
    import socket
    
    
    client  = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    client.connect(('127.0.0.1',8081))
    while True:
        user_inp = input('>>> ').strip()
        client.send(user_inp.encode('utf-8'))  ##发送用户输入的信息
        data = client.recv(1024)  ##接收服务端返回信息,1024代表本次最大接收1024bytes内容
        print('收到服务端信息: ', data)  ##打印接收到的内容
    socket并发编程-客户端

    基于UDP的套接字通讯

    from socket import *            ##将udp的所有模块导入服务端(名称空间)
    
    
    server=socket(AF_INET,SOCK_DGRAM) # 数据报协议UDP
    #1、基于udp协议每发送的一条数据都自带边界,即udp协议没有粘包问题
    #2、基于udp协议的通信,一定是一发对应一收
    
    server.bind(('127.0.0.1',8080))
    
    while True:
        msg,client_addr=server.recvfrom(1024)       ##若接收内容为大于1024bytes,那么大于的部分将被丢弃
        print('收到客户端信息:',msg)
        server.sendto(msg.upper(),client_addr) ##注意这里发送信息和TCP有所不同,必须指定收件人信息,因为UDP不是基于三次握手通讯
    基于UDP-服务端
    from socket import *
    
    client=socket(AF_INET,SOCK_DGRAM)       ##指定数据报协议SOCK_DGRAM
    
    while True:
        msg = input('请输入发送的信息: ').strip()
        client.sendto(msg.encode('utf-8'),('127.0.0.1',8080))       ##指定服务端地址
        res,server_addr=client.recvfrom(1024)           ##若接收内容为大于1024bytes,那么大于的部分将被丢弃
        print('收到服务端信息',res)
    基于UDP-客户端

    结论:  UDP和TCP不同,是不可靠协议,不会进行三次握手建立连接,若服务端/客户端不启动,或网络有问题,客户端/服务端发送也不会抛出异常,发送的数据丢失

            UDP的有效可靠传输512bytes,大于512bytes将不可靠

         UDP没有粘包问题,只有收到或丢失的情况

            涉及到查询,查询的内容小于512建议使用UDP,涉及到重要保存或下载使用TCP

     

     

  • 相关阅读:
    237. Delete Node in a Linked List
    430. Flatten a Multilevel Doubly Linked List
    707. Design Linked List
    83. Remove Duplicates from Sorted List
    160. Intersection of Two Linked Lists
    426. Convert Binary Search Tree to Sorted Doubly Linked List
    142. Linked List Cycle II
    类之间的关系
    初始化块
    明确类和对象
  • 原文地址:https://www.cnblogs.com/swiki/p/9562035.html
Copyright © 2011-2022 走看看