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

     

     

  • 相关阅读:
    关于编码的两个小点(摘)
    c#中类和成员的修饰符介绍
    jquery/js不支持ie9以下版本的方法或属性
    一次Linux系统被攻击的分析过程
    运维堡垒机(跳板机)系统 python
    puppet 3+Unicorn+Nginx安装配置
    用memcache来同步session
    php+memcache实现的网站在线人数统计
    CentOS yum 源的配置与使用
    Bind+DLZ构建企业智能DNS/DNS
  • 原文地址:https://www.cnblogs.com/swiki/p/9562035.html
Copyright © 2011-2022 走看看