Python 基于TCP协议通信的简单套接字编程

前言:学习套接字编程需要掌握的网络基础知识

    包含(TCP/IP 5层模型,TCP协议建立的三次握手与4次断开,网络通信过程等)

    http://www.cnblogs.com/linhaifeng/articles/5937962.html


一、套接字(socket)

    1.1、什么是套接字(socket)

    位于应用层与传输层之间,用来把传输层以下的协议封装好,并提供一个简单的接口,那么在编写基于网络架构的C/S软件的话,就可以考虑使用套接字及按照套接字的标准去编写。

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

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



    1.2、套接字工作流程

扫描二维码关注公众号,回复: 1831216 查看本文章

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


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


    1.3、客户端/服务器架构之 C/S,B/S 模式

        client --------server

        browser -----server

        其中:server端应该注意的问题:

            1、服务端应该保持稳定

            2、服务端要支持并发

            3、服务端要绑定唯一的地址 (ip + port)

二、套接字编程(基于TCP协议通信的C/S模式)   

    TCP是基于链接的,必须先启动服务端,然后再启动客户端去链接服务端

    2.1、TCP服务端套接字函数

    server.bind()     绑定(主机,端口号)到套接字
    server.listen()    开始TCP监听
    server.accept()  被动接受TCP客户的连接,(阻塞式)等待连接的到来

import socket

# 1、买手机                  #AF:address family  INET:internet   合在一起称为"基于网络通信的套接字"   
server=socket.socket(socket.AF_INET,socket.SOCK_STREAM)    #SOCK_STREAM==流式协议:指的就是TCP协议
server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #重用服务端的IP和端口 (如果服务端的IP及端口在短时间内释放掉,那么就把之前的IP及端口重用上,就可以解决端口被占用问题)

# 2、插卡
server.bind(('127.0.0.1',8080))   #服务端的IP地址加端口
# 3、开机
server.listen(5)   #backlog:半连接池(客户端发送一次请求(syn请求)到服务端(服务端会接收syn并回复一个acksyn给客户端),那么在客户端回ack之前,客户端已服务端都是出于半连接状态)
                   #服务端会接收大量客户端的请求,如此多的请求都会堆积在服务器的内存里,如果不做处理的话,服务端的内存会被撑爆,为了保护服务端,不允许大量的客户端请求到达服务端,那么就要对客户端的syn请求加以限制。此处有了半连接池的概念
#什么是半连接池?
'''
在服务端会有一个队列,如果队列的大小为5,代表服务端最高只能接受5个请求数(这里并不是连接数)
当客户端的连接请求到服务端时,请求会到服务端的半连接池里面(最大请求数为5),服务端直接从半连接池里面取出连接请求。(出去一个请求,那么办连接池就会在进去一个连接请求)
半连接池的大小根据服务端的服务器性能来调整(半连接池应该尽量大,但不能无限大)
linux中,内核参数调优:tcp_backlog 指的就是半连接池得大小
'''

# 4、等待电话连接
print('等待连接。。。')
while True:             #等待客户端的SYN请求(也就是客户端的coon操作  ·1)
    coon,client_addr=server.accept()    #accept就是从半连接池里面取出连接请求数,accept对应客户端的connect
    # server.accept() #拿到的就是tcp的连接对象,客户端的ip和端口
    # coon指的是:接受某一个唯一地址的客户端的连接请求

    # print(coon)
    # <socket.socket fd=536, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 54165)>
    #其中:laddr=('127.0.0.1', 8080)    #表示的是服务端的IP和端口
    #其中:raddr=('127.0.0.1', 54165)   #表示的是客户端的IP和端口

    # print(client_addr)
    # ('127.0.0.1', 54165)   # 客户端的IP加端口

    # 5、接收消息
    while True:    #通信循环:服务端与客户端的连接需要交互式的操作
        try:       #客户端挂掉,服务端不受任何影响
            data=coon.recv(1024)  # 1024表示1024字节,客户端发送过来的字节,服务端最多只能收1024            if not data:break
            # coon指的是:与某个唯一地址的客户端连接请求建立后,返回消息给这个客户端
            print('客户端数据:%s' %data)
            # 6、发消息
            coon.send(data.upper())   #接受到客户端消息后,发送消息给客户端
        except ConnectionResetError:
            break

    # 7、挂电话
    coon.close()   #关闭连接状态  (回收的是操作系统的资源)

# 8、关机
server.close()   #关闭服务端   (回收的是操作系统的资源)

# 关闭python程序时,理论上讲会回收掉python程序和操作系统的资源。但是操作系统对应的端口号回不回收取决于操作系统。而不取决于python应用程序
# Linux 可以通过调整内核参数设置。

    1、什么是半连接池(backlog)

        1.1.半连接池的概念:

        1).半连接池(客户端发送一次请求(syn请求)到服务端(服务端会接收syn并回复一个ack及syn给客户端),那么在客户端回ack之前,客户端已服务端都是出于半连接状态)

        2).服务端会接收大量客户端的请求,如此多的请求都会堆积在服务器的内存里,如果不做处理的话,服务端的内存会被撑爆,为了保护服务端,不允许大量的客户端请求到达服务端,那么就要对客户端的syn请求加以限制。此处有了半连接池的概念

        1.2.什么是半连接池

        在服务端会有一个队列,如果队列的大小为5,代表服务端最高只能接受5个请求数(这里并不是连接数)
        当客户端的连接请求到服务端时,请求会到服务端的半连接池里面(最大请求数为5),服务端直接从半连接池里面取出连接请求。(出去一个请求,那么办连接池就会在进去一个连接请求)
    半连接池的大小根据服务端的服务器性能来调整(半连接池应该尽量大,但不能无限大)

        在linux中,内核参数调优:tcp_backlog 指的就是半连接池得大小

    2、服务端消息发送与接收

        1).coon,client_addr=server.accept()    

            accept就是从半连接池里面取出连接请求数,accept对应客户端的connect

        2).server.accept() 

            拿到的就是tcp的连接对象,客户端的ip和端口

            coon指的是:接受某一个唯一地址的客户端的连接请求

        3).print(coon)

        <socket.socket fd=536, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 54165)>

        其中:laddr=('127.0.0.1', 8080)      表示的是服务端的IP和端口
        其中:raddr=('127.0.0.1', 54165)   表示的是客户端的IP和端口

        4).print(client_addr)

        ('127.0.0.1', 54165)     客户端的IP加端口

    3、资源回收

        关闭python程序时,理论上讲会回收掉python程序和操作系统的资源。但是操作系统对应的端口号回不回收取决于操作系统。而不取决于python应用程序。

        在Linux 可以通过调整内核参数设置。


    2.2、TCP客户端套接字函数

    client.connect()        主动初始化TCP服务器连接
    client.connect_ex()  connect()函数的扩展版本,出错时返回出错码,而不是抛出异常

import socket

# 1、买手机
client=socket.socket(socket.AF_INET,socket.SOCK_STREAM)   #SOCK_STREAM==流式协议:指的就是TCP协议

# 2、插卡
client.connect(('127.0.0.1',8080))   #这里的IP和端口都是服务端的

while True:
    msg=input('>>:').strip()
    if not msg:continue
    # 3、发消息
    print('send>>:')
    client.send(msg.encode('utf-8'))   #在网络中发送信息需要通过字节(二进制的方式发送),所以需要encode('utf-8')制定字符集的方式发送
    # client.send 这里的发送指的是:应用程序把数据传给操作系统,由操作系统(经过层层封装)把数据发给服务端,而不是应用程序本身直接发送数据到服务端。
    # 4、收消息
    print('recv>>:')
    data=client.recv(1024)     #接受服务端的消息  #recv里的1024指的是从操作系统的缓存里一次拿出1024个字节的数据
    # client.recv 这里的接收指的是:应用程序(并不是从服务端直接接收数据)而是从自己的操作系统的内存池里面获取从服务端发送过来的数据。
    print(data.decode('utf-8'))   #这里接受的是普通的字符集所以指定字符集'utf-8',如果接收的是系统命令那么就要使用(windows:'gbk',linux:'utf-8')

# 5、挂电话
client.close()   #关闭客户端

    1、TCP通信流程

        1.1、client.send(msg.encode('utf-8'))   

        1).在网络中发送信息需要通过字节(二进制的方式发送),所以需要encode('utf-8')指定字符集的方式发送。

        2).应用程序只负责把数据传给操作系统,不负责数据是否能够到达服务端,由操作系统(经过层层封装)把数据发给服务端,而不是应用程序本身直接发送数据到服务端。

        1.2、data=client.recv(1024)

        1).应用程序拿到的数据(并不是从服务端直接接收数据)而是从自己的操作系统的内存池里面获取,服务端机器的操作系统把数据发送给客户端的机器,客户端机器收到数据后,把数据放在内存里面,然后客户端应用程序从本身机器的内存里面拿到从服务端机器发送过来的数据。

        2).print(data.decode('utf-8')),显示输出内容需要使用decode('utf-8')解码。


    三、粘包现象               #只有TCP有粘包现象,UDP永远不会粘包

        3.1、为什么会发生粘包现象

        1.首先了解socket收发消息的原理



        1).服务端->客户端数据发送流程:服务端的应用程序把数据包发给操作系统,缓存到机器的内存里面,然后由操作系统(经过层层封装)把数据发给客户端的机器,客户端机器收到数据包后存到机器的缓存里面,然后由客户端的应用程序从机器的内存里面获取数据。

        2.粘包的原因

        针对TCP:服务端可以是1K,1K地发送数据,而接收端的应用程序可以2K,2K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。

        基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束

        所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。

        通俗理解:服务端应用程序(等机器内存缓冲区满后)然后一次性发送数据包(发送数据时间间隔很短,数据流很小,会合到一起,产生粘包)到达客户端服务器,而客户端应用程序在接收数据包时(客户端不及时接收缓冲区的包,造成多个包接收),服务端发送了一段数据,客户端只收了一小部分,客户端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包。

        3.TCP 协议  (又称为流式协议)

    TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务(可靠协议)。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法:将多次间隔较小且数据量小的数据合并成一个大的数据块,然后进行封包)。这样接收端就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。

    TCP是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头。

    TCP的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。

    TCP协议的数据并不是 "一发(数据发送)" 对应 "一收(数据接收)",每次发送都是由操作系统决定的,操作系统可能把多个数据合并成一个包发送。

        4.UDP 协议

    UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务(不可靠协议)。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。

    UDP协议在传输层通过本身自带的报头属性,以及一发(发送数据包)一收(接收数据包)的机制解决了数据粘包的问题UDP协议一般不用来传文件,通常用来做与查询相关的数据包的发送,UDP协议稳定有效的数据包传输量最大为512字节(协议本身的原因造成)。

    UDP的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着UDP根本不会粘包,但是会丢数据,不可靠。

    UDP协议一般用在:DNS查询,NTP时间服务器

        5.TCP/UDP 协议的可靠性

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

        6.send(字节流)和recv(1024)及sendall

        recv里指定的1024意思是从缓存里一次拿出1024个字节的数据。
        send的字节流是先放入己端缓存,然后由协议控制将缓存内容发往对端,如果待发送的字节流大小大于缓存剩余空间,那么数据丢失,用sendall就会循环调用send,数据不会丢失。


        3.2、解决粘包现象

        方法:为字节流加上自定义固定长度报头,报头中包含字节流长度,然后一次send到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据(直到收干净为止)。

        通俗解释:字节流加上自定义固定长度报头:客户端在接收时,先去读报头的长度,从而拿到数据包的长度。就相当于手动给数据包划分成一段一段的,客户端每次都会接收完一段在接受另外一段。

        通过在应用层通过封装报头的形式来解决粘包问题,但是并没有改变TCP协议(流式协议)发送数据包的属性。


        3.2.1、解决粘包问题需要先了解一个struct模块的用法:

        该模块可以把一个类型,如数字,转成固定长度的bytes


    使用例子一:

import struct

obj=struct.pack('i',1231231)    #i表示:int 整型,1231231表示:整型的大小为 1231231
print(obj,len(obj))
>>b'\x7f\xc9\x12\x00' 4       #struct.pack得到的是一个二进制形式的数据,长度为4

res=struct.unpack('i',obj)[0]   #使用struct.unpack解包得到整型i的值为1231231
print(res)
>>1231231
    使用例子二:(在解决粘包问题有用到这种方法)
import struct
import json

header_dic={                            #定义字典的格式与内容
    'filenema': 'a.txt',
    'total_size': 123123123123123123123123123123123123123123123123123,
    'md5':'sssxxxadwc123asd123',
}

header_json=json.dumps(header_dic)      #由字典序列化为json格式
print(header_json)                      #得到序列化内容如下
# >>{"filenema": "a.txt", "total_size": 123123123123123123123123123123123123123123123123123, "md5": "sssxxxadwc123asd123"}

header_bytes=header_json.encode('utf-8') #在由json格式转化为bytes格式
print(header_bytes)                      #得到bytes格式内容如下
# >>b'{"filenema": "a.txt", "total_size": 123123123123123123123123123123123123123123123123123, "md5": "sssxxxadwc123asd123"}'

print(len(header_bytes))                 #bytes格式的长度
# >>118

res=struct.pack('i',len(header_bytes))   #使用struct.pack'i(int)'及长度len(header_bytes)转换成固定长度的bytes
print(res,len(res))                      #bytes的值为:b'v\x00\x00\x00'
# >>b'v\x00\x00\x00' 4                 #bytes的长度为:4

obj=struct.unpack('i',res)[0]            #使用struct.unpack解包得到bytes格式的长度(得到的值是一个元组:(118,)),这里只需要取118即可
print(obj)
# >>118

    解决粘包问题--初级版:

    服务端代码:

from socket import *
import subprocess
import struct

server=socket(AF_INET,SOCK_STREAM)
server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
server.bind(('127.0.0.1',8086))
server.listen(5)

print('等待连接。。。')
while True:
    coon,client_addr=server.accept()
    while True:  #通信循环
        try:
            cmd=coon.recv(1024)
            if not cmd:break
            obj=subprocess.Popen(cmd.decode('utf-8'),shell=True,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE,
                             )
            stdout=obj.stdout.read()
            stderr=obj.stderr.read()         

            # 1、先发送固定长度的报头  #如何制作固定长度的报头(用到struct)
            total_size=len(stdout) + len(stderr)      #数据的总长度
             coon.send(struct.pack('i',total_size))    #把包含数据长度的报头发送到服务端

            # 2、发送真实数据
             coon.send(stdout)
            coon.send(stderr)
        except ConnectionResetError:
            break
    coon.close()   #关闭连接状态  (回收的是操作系统的资源)

server.close()   #关闭服务端   (回收的是操作系统的资源)

    客户端代码:

from socket import *
import struct

client=socket(AF_INET,SOCK_STREAM)   #SOCK_STREAM==流式协议:指的就是TCP协议
client.connect(('127.0.0.1',8086))   #这里的IP和端口都是服务端的

while True:
    cmd=input('>>:').strip()
    if not cmd:continue
    client.send(cmd.encode('utf-8'))   #在网络中发送信息需要通过字节(二进制的方式发送),所以需要encode('utf-8')制定字符集的方式发送
    print('send..')

    # 1、先收报头,从报头里取出对真实数据的描述信息
    header=client.recv(4)                      #接收4个字节即可,struct.pack('i',1231231)的长度为4,所以只用接收4个即可
    total_sisz=struct.unpack('i', header)[0]   #解包报头,拿到数据的总长度

    # 2、循环接收真实数据,直到收完为止
    recv_size=0                        #接收数据包的大小
    res=b''                            #把接收到的数据包拼接到一起
    while recv_size < total_sisz:
        recv_data=client.recv(1024)    #循环接收服务端传过来的数据
        res+=recv_data                  #res把接收到的数据全部拼接起来
        recv_size+=len(recv_data)       #接收到的数据的长度直到等于数据的总长度为止

    print(res.decode('gbk'))  #就收客户端操作系统(windows默认使用gbk)发过来的数据,想要输出到屏幕得使用gbk解码

client.close()   #关闭客户端

    解决粘包问题--终极版

    我们可以把报头做成字典,字典里包含将要发送的真实数据的详细信息,然后json序列化,然后用struck将序列化后的数据长度打包成4个字节(4个自己足够用了)

    发送时(服务端):
        先发报头长度。
        再编码报头内容然后发送。
        最后发真实内容。


    接收时(客户端):
        先手报头长度,用struct取出来。
        根据取出的长度收取报头内容,然后解码,反序列化。

        从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容。


服务端代码:

from socket import *
import subprocess
import struct
import json

server=socket(AF_INET,SOCK_STREAM)
server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
server.bind(('127.0.0.1',8086))
server.listen(5)

print('等待连接。。。')
while True:
    coon,client_addr=server.accept()

    # 通信循环
    while True:
        try:
            cmd=coon.recv(1024)
            if not cmd:break
            obj=subprocess.Popen(cmd.decode('utf-8'),shell=True,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE,
                             )
            stdout=obj.stdout.read()
            stderr=obj.stderr.read()

            #制作报头
            header_dic = {                                   #设置报头为字典格式
                'filenema': 'a.txt',                         #文件名
                'total_size': len(stdout) + len(stderr),     #数据总长度
                'md5': 'sssxxxadwc123asd123',                #md5值
            }

            header_json = json.dumps(header_dic)             #把报头,序列化为json格式
            header_bytes = header_json.encode('utf-8')       #在由json格式转化为bytes格式(数据包发送通过bytes形式发送)

            #1、先发送报头的长度(客户端拿到报头的长度后可以知道要接受的数据大小)
            coon.send(struct.pack('i',len(header_bytes)))    #struct.pack用法:#i表示:int 整型,len(header_bytes)表示:报头的长度(int类型)

            #2、在发送报头(报头的内容)
            coon.send(header_bytes)

            #3、最后发送真实数据
            coon.send(stdout)
            coon.send(stderr)
        except ConnectionResetError:
            break

    # 关闭连接状态  (回收的是操作系统的资源)
    coon.close()

#关闭服务端   (回收的是操作系统的资源)
server.close()

客户端代码:

from socket import *
import struct
import json

client=socket(AF_INET,SOCK_STREAM)              #SOCK_STREAM==流式协议:指的就是TCP协议
client.connect(('127.0.0.1',8086))              #这里的IP和端口都是服务端的

while True:
    cmd=input('>>:').strip()
    if not cmd:continue
    client.send(cmd.encode('utf-8'))            #在网络中发送信息需要通过字节(二进制的方式发送),所以需要encode('utf-8')制定字符集的方式发送
    print('send..')

    # 1、先收报头的长度(服务端先发送的是报头的长度,所有要先接收报头的长度)
    obj=client.recv(4)                          #报头长度
    header_size=struct.unpack('i',obj)[0]       #拿到报头长度后,通过struct.unpack拿到报头的大小(即服务端header_dic的大小)

    # 2、在接收报头
    header_bytes=client.recv(header_size)       #通过client.recv接收报头
    header_json=header_bytes.decode('utf-8')    #接收报头后对报头的格式做反序列化处理(因为报头在服务端被json序列化了)
    header_dic=json.loads(header_json)          #通过json.loads拿到报头的字典格式及内容
    print(header_dic)

    total_size=header_dic['total_size']         #拿到了真实数据的总长度

    # 3、循环接收真实数据,直到收完为止
    recv_size=0                                 #接收数据包的大小
    res=b''                                     #把接收到的数据包拼接到一起
    while recv_size < total_size:
        recv_data=client.recv(1024)             #循环接收服务端传过来的数据
        res+=recv_data                          #res把接收到的数据全部拼接起来
        recv_size+=len(recv_data)               #接收到的数据的长度直到等于数据的总长度为止

    print(res.decode('gbk'))                    #就收客户端操作系统(windows默认使用gbk)发过来的数据,想要输出到屏幕得使用gbk解码

# 5、关闭客户端
client.close()   


四、套接字编程(基于UDP协议通信套接字编程)  

    服务端代码:

from socket import *

server=socket(AF_INET,SOCK_DGRAM)    #SOCK_DGRAM == 数据报协议(UDP协议) -- 在发送数据库时每一条数据UDP协议都会做报头处理,
                                         #那么在接受端,就会根据数据报的内容接受数据,而不会发生粘包问题。
#1、基于UDP协议每发送的一条数据都自带边界,即UDP协议没有粘包问题,
#2、基于UDP协议的通信,一定是一发对应一收

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

while True:
    msg,client_addr=server.recvfrom(1024)    #server.recvfrom(1024)拿到的就是"客户端发送过来的数据,及IP+端口"
    server.sendto(msg.upper(),client_addr)

    客户端代码:

from socket import *

client=socket(AF_INET,SOCK_DGRAM)

while True:
    msg=input('>>:').strip()
    client.sendto(msg.encode('utf-8'),('127.0.0.1',8092))   #UDP协议没有与服务端建立连接,所以在发送数据时需要指定服务端的IP和端口

    res,server_addr=client.recvfrom(1024)
    print(res)
    # msg,client_addr=server.recvfrom(1024)
    # server.sendto(msg.upper(),client_addr)

证明UDP协议是一收一发机制:

    服务端代码:  连续接收三个数据,但是数据没有粘包

from socket import *

server=socket(AF_INET,SOCK_DGRAM)
server.bind(('127.0.0.1',8092))

msg1,client_addr=server.recvfrom(1024)
print(msg1)
msg2,client_addr=server.recvfrom(1024)
print(msg2)
msg3,client_addr=server.recvfrom(1024)
print(msg3)

>>b'hello'          #客户端发送一条数据,服务端接收一条数据,没有粘包
>>b'world'          #客户端发送一条数据,服务端接收一条数据,没有粘包
>>b'sudada'         #客户端发送一条数据,服务端接收一条数据,没有粘包
    客户端代码: 连续发送三个数据
from socket import *

client=socket(AF_INET,SOCK_DGRAM)
while True:
    client.sendto('hello'.encode('utf-8'),('127.0.0.1',8092))    #发送数据
    client.sendto('world'.encode('utf-8'),('127.0.0.1',8092))    #发送数据
    client.sendto('sudada'.encode('utf-8'),('127.0.0.1',8092))   #发送数据

    基于UDP协议编写的套接字编程中,服务端不能实现并发的效果(看起来像是并发的效果,实际上并不是。因为UDP协议是一发对应一收的,数据发完就删除掉,每次都可以快速执行,所以看起来像是并发的效果。)


当客户端发送的数据量大于服务端接收的数据量时,会出现报错,提示:"用户接收数据报的缓冲区比数据包小",这种情况在不同的操作系统分不同的情况:

    1、在windows系统:会报错。

    2、在linux系统:能接收多少就接收多少,接收不了的都丢弃,不会报错。

from socket import *

server=socket(AF_INET,SOCK_DGRAM)
server.bind(('127.0.0.1',8092))

msg1,client_addr=server.recvfrom(1)
print(msg1)
msg2,client_addr=server.recvfrom(1)
print(msg2)
msg3,client_addr=server.recvfrom(1)
print(msg3)

>>OSError: [WinError 10040] 一个在数据报套接字上发送的消息大于内部消息缓冲区或其他一些网络限制,或该用户用于接收数据报的缓冲区比数据报小。




猜你喜欢

转载自blog.csdn.net/sinat_29214327/article/details/80574955