Socket 网络编程

Posted dongye95

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Socket 网络编程相关的知识,希望对你有一定的参考价值。


本节内容:

  1. Socket语法及相关
  2. SocketServer实现多并发 

一、网络基础

网络基础链接

1.1 一个程序如何在网络上找到另一个程序?

  首先,程序必须要启动,其次,必须有这台机器的地址,我们都知道我们人的地址大概就是国家\\省\\市\\区\\街道\\楼\\门牌号这样字。那么每一台联网的机器在网络上也有自己的地址,它的地址是怎么表示的呢?

  就是使用一串数字来表示的,例如:100.4.5.6

什么是IP地址

IP地址是指互联网协议地址(英语:Internet Protocol Address,又译为网际协议地址),是IP Address的缩写。IP地址是IP协议提供的一种统一的地址格式,
它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。 IP地址是一个32位的二进制数,通常被分割为4个“8位二进制数”(也就是4个字节)。IP地址通常用“点分十进制”表示成(a.b.c.d)的形式,
其中,a,b,c,d都是0~255之间的十进制整数。例:点分十进IP地址(100.4.5.6),实际上是32位二进制数(01100100.00000100.00000101.00000110)。

什么是端口

"端口"是英文port的意译,可以认为是设备与外界通讯交流的出口。

在windows上查看端口占用的情况

netstat -aon|findstr "49157"

因此ip地址精确到具体的一台电脑,而端口精确到具体的程序。

1.2 ios七层模型

1.3 socket概念

socket层

理解socket

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

  其实站在你的角度上看,socket就是一个模块。我们通过调用模块中已经实现的方法建立两个进程之间的连接和通信。 也有人将socket说成ip+port,因为ip是用来标识互联网中的一台主机的位置,而port是用来标识这台机器上的一个应用程序。 所以我们只要确立了ip和port就能找到一个应用程序,并且使用socket模块来与之通信。

1.4 套接字(socket)的发展史

  套接字起源于 20 世纪 70 年代加利福尼亚大学伯克利分校版本的 Unix,即人们所说的 BSD Unix。 因此,有时人们也把套接字称为“伯克利套接字”或“BSD 套接字”。一开始,套接字被设计用在同 一台主机上多个应用程序之间的通讯。这也被称进程间通讯,或 IPC。套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的。 

1 基于文件类型的套接字家族

  套接字家族的名字:AF_UNIX

  unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信

2 基于网络类型的套接字家族

  套接字家族的名字:AF_INET

  (还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET)

1.5 tcp协议和udp协议

TCP(Transmission Control Protocol)可靠的、面向连接的协议(eg:打电话)、传输效率低全双工通信(发送缓存&接收缓存)、面向字节流。使用TCP的应用:Web浏览器;电子邮件、文件传输程序。

UDP(User Datagram Protocol)不可靠的、无连接的服务,传输效率高(发送前时延小),一对一、一对多、多对一、多对多、面向报文,尽最大努力服务,无拥塞控制。使用UDP的应用:域名系统 (DNS);视频流;IP语音(VoIP)。

二、套接字(socket)初使用

2.1 基于TCP协议的socket

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

server端

import socket
sk = socket.socket()
sk.bind((\'127.0.0.1\',8898))  #把地址绑定到套接字
sk.listen()          #监听链接
conn,addr = sk.accept() #接受客户端链接
ret = conn.recv(1024)  #接收客户端信息
print(ret)       #打印客户端信息
conn.send(b\'hi\')        #向客户端发送信息
conn.close()       #关闭客户端套接字
sk.close()        #关闭服务器套接字(可选)

client端

import socket
sk = socket.socket()           # 创建客户套接字
sk.connect((\'127.0.0.1\',8898))    # 尝试连接服务器
sk.send(b\'hello!\')
ret = sk.recv(1024)         # 对话(发送/接收)
print(ret)
sk.close()            # 关闭客户套接字

问题:有的同学在重启服务端时可能会遇到

解决方法:

#加入一条socket配置,重用ip和端口
import socket
from socket import SOL_SOCKET,SO_REUSEADDR
sk = socket.socket()
sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加
sk.bind((\'127.0.0.1\',8898))  #把地址绑定到套接字
sk.listen()          #监听链接
conn,addr = sk.accept() #接受客户端链接
ret = conn.recv(1024)   #接收客户端信息
print(ret)              #打印客户端信息
conn.send(b\'hi\')        #向客户端发送信息
conn.close()       #关闭客户端套接字
sk.close()        #关闭服务器套接字(可选)

send 与sendall的区别与使用方法

  socket.send(string[, flags])  发送TCP数据,返回发送的字节大小。这个字节长度可能少于实际要发送的数据的长度。换句话说,这个函数执行一次,并不一定能发送完给定的数据,可能需要重复多次才能发送完成。

buffer = \'Hello world\\n\'
while buffer:
    bytes = sock.send(buffer)
    buffer = buffer[bytes:]

  socket.sendall(string[, flags])   看懂了上面那个,这个函数就容易明白了。发送完整的TCP数据,成功返回None,失败抛出异常

sock.sendall(\'Hello world\\n\') # 一次性全发过去

实例:智能机器人

# server 端
import socket

ip_port = (\'127.0.0.1\',8888)  # 确定 IP 和 端口号,127.0.0.1本地回环
sk = socket.socket()
sk.bind(ip_port)  # 将套接字绑定到地址
sk.listen(5)  # 开始监听传入连接。backlog指定在拒绝连接之前,可以挂起的最大连接数量。

while True:
    conn,address =  sk.accept()  # 接受连接并返回(conn,address),其中conn是新的套接字对象,可以用来接收和发送数据。address是连接客户端的地址。
    conn.sendall(bytes(\'欢迎致电 10086,请输入1xxx,0转人工服务.\',"utf-8"))
        # 需要注意,socket传输的只能是bytes类型数据,默认的str类型不能传输。得转一下。

    Flag = True
    while Flag:
        data = conn.recv(1024)  每次接受1024个字节
        if str(data,"utf-8") == \'exit\':  # 接受的是bytes类型数据,得转成str类型,再比较。
            Flag = False
        elif str(data,"utf-8") == \'0\':
            conn.sendall(bytes(\'通过可能会被录音.balabala一大推\',"utf-8"))
        else:
            conn.sendall(bytes(\'请重新输入.\',"utf-8"))
    conn.close()
# client 端 
import socket

ip_port = (\'127.0.0.1\',8888)  # 确定 IP 和 端口号,127.0.0.1本地回环
sk = socket.socket()
sk.connect(ip_port)  # 连接到address处的套接字。一般,address的格式为元组(hostname,port),如果连接出错,返回socket.error错误
sk.settimeout(5)
"""设置套接字操作的超时期,timeout是一个浮点数,单位是秒。
值为None表示没有超时期。一般,超时期应该在刚创建套接字时设置,
因为它们可能用于连接的操作(如 client 连接最多等待5s )
"""
while True:
    data = sk.recv(1024)
    print(\'receive:\',str(data,"utf-8"))
    inp = input(\'please input:\')
    sk.sendall(bytes(inp,"utf-8"))
    if inp == \'exit\':
        break

sk.close()

实例2:连续聊天

# client 端

import socket
sk = socket.socket()
ip_port = ("127.0.0.1",8888)
sk.bind(ip_port)
sk.listen(5)
print("服务器端启动。。。")
conn,addr = sk.accept()

while True:
    try:
        client_data = conn.recv(1024) # 防止接收时出现意外
    except Exception:
        print("不知道why,总之意外中断!")
        break
    if str(client_data,"utf-8") == "exit":
        break
    print(str(client_data,"utf-8"))
    conn.sendall(bytes("服务器端接收到!","utf-8"))

conn.close()

# client 端
import socket

sk = socket.socket()
ip_port = ("127.0.0.1",8888)
sk.connect(ip_port)
print("客户端端启动。。。")
while True:
    inp = input("输入你要传输的数据:")
    sk.send(bytes(inp, "utf-8"))
    if inp == "exit":  # 先send数据,然后再判断,直接break,服务器端ConnectionAbortedError
        break
    server_data = sk.recv(1024)
    if str(server_data,"utf-8") == "exit":
        break
    print(str(server_data,"utf-8"))
    sk.sendall(bytes("客户端接收到!","utf-8"))

sk.close()

实例3:传送命令

#------------------------------------------------server
#------------------------------------------------
import socket
import subprocess
ip_port = (\'127.0.0.1\',8879)
sk = socket.socket()
sk.bind(ip_port)
sk.listen(5)
print ("服务端启动...")
while True:
    conn,address = sk.accept()
    while True:
        try:

            client_data=conn.recv(1024)
        except Exception:
            break
        print (str(client_data,"utf8"))
        print ("waiting...")
        # server_response=input(">>>")
        # conn.sendall(bytes(server_response,"utf8"))
        cmd=str(client_data,"utf8").strip()
        cmd_call=subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE)
        cmd_result=cmd_call.stdout.read()
        if len(cmd_result)==0:
            cmd_result=b"no output!"
        conn.sendall(cmd_result)
        print(\'send data size\',len(cmd_result))
        print(\'******************\')
        print(\'******************\')
        print(\'******************\')

    conn.close()
    
#------------------------------------------------client 
#------------------------------------------------
import socket
ip_port = (\'127.0.0.1\',8879)
sk = socket.socket()
sk.connect(ip_port)
print ("客户端启动:")
while True:
    inp = input(\'cdm:>>>\').strip( )
    if len(inp)==0:
        continue
    if inp=="q":
        break
    sk.sendall(bytes(inp,"utf8"))
    server_response=sk.recv(1024)
    print (str(server_response,"gbk"))
    print(\'receive data size\',len(server_response))
    if inp == \'exit\':
        break
sk.close()

实例4:文件上传

文件目录格式如下:

总文件
     |___images
     |         |___1.png
     |
     |___new_images
     |
     |
     |___server.py
     |
     |
     |___client.py

以下程序可以把 images文件夹下的 1.png 复制到 new_images 文件夹下。

#----------------------------------------------server
#----------------------------------------------
import socket,os
ip_port=("127.0.0.1",8898)
sk=socket.socket()
sk.bind(ip_port)
sk.listen(5)
BASE_DIR=os.path.dirname(os.path.abspath(__file__))  # 现目录的 dirname
# BASE_DIR = os.path.abspath("..")  # 这样写更能理解。
print("服务器端启动....")

while True:
    print("waiting connect")
    conn,addr=sk.accept()
    flag = True
    while flag:

            client_bytes = conn.recv(1024)
            client_str = str(client_bytes, "utf8")
            func, file_byte_size, filename = client_str.split("|",2)
                # post_info = "post|%s|%s"%(file_byte_size, file_name)  client端发送格式

            path = os.path.join(BASE_DIR,\'new_images\',filename)
            has_received = 0
            file_byte_size = int(file_byte_size)  # file_byte_size已经为str格式,转成int就行

            f=open(path,"wb")
            while has_received<file_byte_size:
                data=conn.recv(1024)
                f.write(data)
                has_received+=len(data)
            print("ending")
            f.close()

#----------------------------------------------client
#----------------------------------------------
import socket
import re, os, sys
ip_port = ("127.0.0.1", 8898)
sk = socket.socket()
sk.connect(ip_port)
BASE_DIR = os.path.dirname(os.path.abspath(__file__))  # 现目录的 dirname
# BASE_DIR = os.path.abspath("..")  # 这样写更能理解。
print("客户端启动....")

while True:
    inp=input("please input:")  # 格式为 post|images/1.png

    if inp.startswith("post"):

        method, local_path=inp.split("|", 1)  # method = "post",local_path = "images/1.png"
        local_path = os.path.join(BASE_DIR, local_path)  # local_path 现在为图片绝对路径
        file_byte_size = os.stat(local_path).st_size  # 得到文件大小
            # file_byte_size = os.path.getsize(local_path) 更好理解
        file_name = os.path.basename(local_path)  # 得到图片名称,1.png 。不带images
        post_info = "post|%s|%s"%(file_byte_size, file_name)# 返回文件信息元组。(大小,名称)
        sk.sendall(bytes(post_info, "utf8"))
        has_sent = 0
        file_obj = open(local_path, "rb")  # 以二进制打开图片(文件)
        while has_sent != file_byte_size:  #判断文件是否传完
            data = file_obj.read(1024)
            sk.sendall(data)  # 每次发送1024字节。以免文件过大,一次传不了。
            has_sent += len(data)
        file_obj.close()
        print("上传成功")

2.2 基于UDP协议的socket

udp是无链接的,启动服务之后可以直接接受消息不需要提前建立链接

2.2.1 简单使用

server端

import socket
udp_sk = socket.socket(type=socket.SOCK_DGRAM)   #创建一个服务器的套接字
udp_sk.bind((\'127.0.0.1\',9000))        #绑定服务器套接字
msg,addr = udp_sk.recvfrom(1024)
print(msg)
udp_sk.sendto(b\'hi\',addr)                 # 对话(接收与发送)
udp_sk.close()                         # 关闭服务器套接字

client端

import socket
ip_port=(\'127.0.0.1\',9000)
udp_sk=socket.socket(type=socket.SOCK_DGRAM)
udp_sk.sendto(b\'hello\',ip_port)
back_msg,addr=udp_sk.recvfrom(1024)
print(back_msg.decode(\'utf-8\'),addr)

实例:QQ聊天

# server 端

#_*_coding:utf-8_*_
import socket
ip_port=(\'127.0.0.1\',8081)
udp_server_sock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
udp_server_sock.bind(ip_port)

while True:
    qq_msg,addr=udp_server_sock.recvfrom(1024)
    print(\'来自[%s:%s]的一条消息:\\033[1;44m%s\\033[0m\' %(addr[0],addr[1],qq_msg.decode(\'utf-8\')))
    back_msg=input(\'回复消息: \').strip()

    udp_server_sock.sendto(back_msg.encode(\'utf-8\'),addr)

# client 端

#_*_coding:utf-8_*_
import socket
BUFSIZE=1024
udp_client_socket=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)

qq_name_dic={
    \'金老板\':(\'127.0.0.1\',8081),
    \'哪吒\':(\'127.0.0.1\',8081),
    \'egg\':(\'127.0.0.1\',8081),
    \'yuan\':(\'127.0.0.1\',8081),
}


while True:
    qq_name=input(\'请选择聊天对象: \').strip()
    while True:
        msg=input(\'请输入消息,回车发送,输入q结束和他的聊天: \').strip()
        if msg == \'q\':break
        if not msg or not qq_name or qq_name not in qq_name_dic:continue
        udp_client_socket.sendto(msg.encode(\'utf-8\'),qq_name_dic[qq_name])

        back_msg,addr=udp_client_socket.recvfrom(BUFSIZE)
        print(\'来自[%s:%s]的一条消息:\\033[1;44m%s\\033[0m\' %(addr[0],addr[1],back_msg.decode(\'utf-8\')))

udp_client_socket.close()

实例2:时间服务器

# server 端

# _*_coding:utf-8_*_
from socket import *
from time import strftime

ip_port = (\'127.0.0.1\', 9000)
bufsize = 1024

tcp_server = socket(AF_INET, SOCK_DGRAM)
tcp_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
tcp_server.bind(ip_port)

while True:
    msg, addr = tcp_server.recvfrom(bufsize)
    print(\'===>\', msg)

    if not msg:
        time_fmt = \'%Y-%m-%d %X\'
    else:
        time_fmt = msg.decode(\'utf-8\')
    back_msg = strftime(time_fmt)

    tcp_server.sendto(back_msg.encode(\'utf-8\'), addr)

tcp_server.close()

# client

#_*_coding:utf-8_*_
from socket import *
ip_port=(\'127.0.0.1\',9000)
bufsize=1024

tcp_client=socket(AF_INET,SOCK_DGRAM)


while True:
    msg=input(\'请输入时间格式(例%Y %m %d)>>: \').strip()
    tcp_client.sendto(msg.encode(\'utf-8\'),ip_port)

    data=tcp_client.recv(bufsize)

2.3 socket参数的详解

socket.socket(family=AF_INET,type=SOCK_STREAM,proto=0,fileno=None)

创建socket对象的说明

family 地址系列应为AF_INET(默认值),AF_INET6,AF_UNIX,AF_CAN或AF_RDS。
(AF_UNIX 域实际上是使用本地 socket 文件来通信)
type 套接字类型应为SOCK_STREAM(默认值),SOCK_DGRAM,SOCK_RAW或其他SOCK_常量之一。
SOCK_STREAM 是基于TCP的,有保障的(即能保证数据正确传送到对方)面向连接的SOCKET,多用于资料传送。 
SOCK_DGRAM 是基于UDP的,无保障的面向消息的socket,多用于在网络上发广播信息。
proto 协议号通常为零,可以省略,或者在地址族为AF_CAN的情况下,协议应为CAN_RAW或CAN_BCM之一。
fileno 如果指定了fileno,则其他参数将被忽略,导致带有指定文件描述符的套接字返回。
与socket.fromfd()不同,fileno将返回相同的套接字,而不是重复的。
这可能有助于使用socket.close()关闭一个独立的插座。

三、黏包

3.1 黏包现象

  同时执行多条命令之后,得到的结果很可能只有一部分,在执行其他命令的时候又接收到之前执行的另外一部分结果,这种显现就是黏包。

  我们来写一个远程执行命令的程序,写一个socket client端在windows端发送指令,一个socket server在Linux端执行命令并返回结果给客户端执行命令的话,肯定是用我们学过的subprocess模块啦,但注意注意注意

res=subprocess.Popen(cmd.decode(\'utf-8\'),
shell=True,
stderr=subprocess.PIPE,
stdout=subprocess.PIPE)

  命令结果的编码是以当前所在的系统为准的,如果是windows,那么res.stdout.read()读出的就是GBK编码的,在接收端需要用GBK解码,且只能从管道里读一次结果  

3.1.1 基于tcp协议实现的黏包

# server 端

#_*_coding:utf-8_*_
from socket import *
import subprocess

ip_port=(\'127.0.0.1\',8888)
BUFSIZE=1024

tcp_socket_server=socket(AF_INET,SOCK_STREAM)
tcp_socket_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
tcp_socket_server.bind(ip_port)
tcp_socket_server.listen(5)

while True:
    conn,addr=tcp_socket_server.accept()
    print(\'客户端\',addr)

    while True:
        cmd=conn.recv(BUFSIZE)
        if len(cmd) == 0:break

        res=subprocess.Popen(cmd.decode(\'utf-8\'),shell=True,
                         stdout=subprocess.PIPE,
                         stdin=subprocess.PIPE,
                         stderr=subprocess.PIPE)

        stderr=res.stderr.read()
        stdout=res.stdout.read()
        conn.send(stderr)
        conn.send(stdout)

# client 端

#_*_coding:utf-8_*_
import socket
BUFSIZE=1024
ip_port=(\'127.0.0.1\',8888)

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=s.connect_ex(ip_port)    #connect()函数的扩展版本,出错时返回出错码,而不是抛出异常

while True:
    msg=input(\'>>: \').strip()
    if len(msg) == 0:continue
    if msg == \'quit\':break

    s.send(msg.encode(\'utf-8\')) #如果在win下,utf-8换成gbk
    act_res=s.recv(BUFSIZE)

    print(act_res.decode(\'utf-8\'),end=\'\')

3.1.2 基于udp协议实现的黏包

# server 端

#_*_coding:utf-8_*_
from socket import *
import subprocess

ip_port=(\'127.0.0.1\',9000)
bufsize=1024

udp_server=socket(AF_INET,SOCK_DGRAM)
udp_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
udp_server.bind(ip_port)

while True:
    #收消息
    cmd,addr=udp_server.recvfrom(bufsize)
    print(\'用户命令----->\',cmd)

    #逻辑处理
    res=subprocess.Popen(cmd.decode(\'utf-8\'),shell=True,stderr=subprocess.PIPE,stdin=subprocess.PIPE,stdout=subprocess.PIPE)
    stderr=res.stderr.read()
    stdout=res.stdout.read()

    #发消息
    udp_server.sendto(stderr,addr)
    udp_server.sendto(stdout,addr)
udp_server.close()


# client 端

from socket import *
ip_port=(\'127.0.0.1\',9000)
bufsize=1024

udp_client=socket(AF_INET,SOCK_DGRAM)


while True:
    msg=input(\'>>: \').strip()
    udp_client.sendto(msg.encode(\'utf-8\'),ip_port)
    err,addr=udp_client.recvfrom(bufsize)
    out,addr=udp_client.recvfrom(bufsize)  # bufsize = 1024 ,有时候不够大
    # Windows运行会出现 OSError: [WinError 10040] 一个在数据报套接字上发送的消息大于内部消息缓冲区或其他一些网络限制,或该用户用于接收数据报的缓冲区比数据报小。
    # 原因是因为
recvfrom收的数据小于sendinto发送的数据时,在mac和linux系统上数据直接丢失,在windows系统上发送的比接收的大直接报错
    # 只有sendinto发送数据没有recvfrom收数据,数据丢失。所有其实就算没有服务端,客户端也可以使劲发,只是数据都被丢弃而已。
if err:
        print(\'error : %s\'%err.decode(\'utf-8\'),end=\'\')
    if out:
        print(out.decode(\'utf-8\'), end=\'\')

注意:只有TCP有粘包现象,UDP永远不会粘包

3.2 黏包成因

3.2.1 TCP协议中的数据传递

tcp协议的拆包机制

当发送端缓冲区的长度大于网卡的MTU时,tcp会将这次发送的数据拆成几个数据包发送出去。 
MTU是Maximum Transmission Unit的缩写。意思是网络上传送的最大数据包。MTU的单位是字节。 大部分网络设备的MTU都是1500。
如果本机的MTU比网关的MTU大,大的数据包就会被拆开来传送,这样会产生很多数据包碎片,增加丢包率,降低网络速度。

面向流的通信特点和Nagle算法

TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。
收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。
这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。 
对于空消息:tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),也可以被发送,udp协议会帮你封装上消息头发送过去。 可靠黏包的tcp协议:tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。
数据是可靠的,但是会粘包。

基于tcp协议特点的黏包现象成因

socket数据传输过程中的用户态与内核态说明

 

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

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

  此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。

3.2.2 UDP不会发生黏包

UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。 不会使用块的合并优化算法, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。 
对于空消息:tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),也可以被发送,udp协议会帮你封装上消息头发送过去。 
不可靠不黏包的udp协议:udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y;x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠。

补充说明:

# udp和tcp一次发送数据长度的限制    

    用UDP协议发送时,用sendto函数最大能发送数据的长度为:65535- IP头(20) – UDP头(8)=65507字节。用sendto函数发送数据时,如果发送数据长度大于该值,则函数会返回错误。(丢弃这个包,不进行发送)
    用TCP协议发送时,由于TCP是数据流协议,因此不存在包大小的限制(暂不考虑缓冲区的大小),这是指在用send函数时,数据长度参数不受限制。而实际上,
所指定的这段数据并不一定会一次性发送出去,如果这段数据比较长,会被分段发送,如果比较短,可能会等待和下一次数据一起发送。

3.2.3 会发生黏包的两种情况

情况一 发送方的缓存机制

  发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)

# server 端
#_*_coding:utf-8_*_
from socket import *
ip_port=(\'127.0.0.1\',8080)

tcp_socket_server=socket(AF_INET,SOCK_STREAM)
tcp_socket_server.bind(ip_port)
tcp_socket_server.listen(5)


conn,addr=tcp_socket_server.accept()


dat

以上是关于Socket 网络编程的主要内容,如果未能解决你的问题,请参考以下文章

VSCode自定义代码片段——JS中的面向对象编程

VSCode自定义代码片段9——JS中的面向对象编程

VSCode自定义代码片段14——Vue的axios网络请求封装

VSCode自定义代码片段14——Vue的axios网络请求封装

VSCode自定义代码片段14——Vue的axios网络请求封装

Python--网络编程-----socket代码实例