【python 第13日】网络通信socket socketserver

需要弄清的几个问题:

1、什么时候用tcp,什么时候用udp?

2、tcp快还是udp快?

3、视频,直播用udp还是tcp,效果差异有多少?安全性能有多少?有待完善

4、如果解决粘包问题

5、一次传输多少,会造成缓冲区丢包

6、在linux和windows里,对于空包,回撤,和直接断开连接的处理不同

7、端口被占用了怎么办?

8、能不能直接操作数据缓冲区,以防丢包

9、测试一下用tcp和udp传输一个G的文件,多久能传完,传输命令

本文结构

socket定义------->TCP------------>UDP--------------->多任务-------------------->socketserver

socket

socket 套接字

socket(简称 套接字) 是进程间通信一个工具,它能实现把数据从一方传输到另外一方,完成不同电脑上进程之间的通信, 它好比数据的搬运工

基本流程:

简单的服务端和客户端

##############################服务器端
import socket
ip_port = ("127.0.0.1", 9999)

sk = socket.socket()             #建立一个套接字,用来接收请求
sk.bind(ip_port)                 #绑定一个端口
sk.listen(2)                     #接听,最大能候补接听5个

conn, addr = sk.accept()         #conn是连接的套接字, addr是对方的ip_port
print(conn, addr)                #<socket.socket fd=616, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 9999), raddr=('127.0.0.1', 56779)> ('127.0.0.1', 56779)

conn.sendall(bytes("hello world", encoding="utf8"))     #一定给conn发送数据,发送的是字节,
print("发送完毕")

############################客户端
import  socket
ip_port = ("127.0.0.1", 9999)

sk = socket.socket()
sk.connect(ip_port)                       #建立一个套接字,连接服务器端口

print(sk.recv(1024).decode("utf8"))       #等待接收

 socket常用函数以及属性

基本参数:

Socket函数使用的格式为:socket(family,type[,protocol])

  参数一:family 指定应用程序使用的通信协议的协议族,对于TCP/IP协议族,该参数为AF_INET,以下为该参数的一些常用选项

  参数二:type 是要创建套接字的类型,以下为该参数的一些常用选项
 

   参数三:protocol 指明所要接收的协议类型,通常为0或者不填。

以下为该参数的一些常用选项

使用例子:

   创建TCP Socket:s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)

   创建UDP Socket:s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)

常用函数:

 

sk.bind(address)
  s.bind(address) 将套接字绑定到地址。address地址的格式取决于地址族。在AF_INET下,以元组(host,port)的形式表示地址。
sk.listen(backlog)
  开始监听传入连接。backlog指定在拒绝连接之前,可以挂起的最大连接数量。
      backlog等于5,表示内核已经接到了连接请求,但服务器还没有调用accept进行处理的连接个数最大为5
      这个值不能无限大,因为要在内核中维护连接队列
sk.setblocking(bool)
  是否阻塞(默认True),如果设置False,那么accept和recv时一旦无数据,则报错。
sk.accept()
  接受连接并返回(conn,address),其中conn是新的套接字对象,可以用来接收和发送数据。address是连接客户端的地址。
  接收TCP 客户的连接(阻塞式)等待连接的到来
sk.connect(address)
  连接到address处的套接字。一般,address的格式为元组(hostname,port),如果连接出错,返回socket.error错误。
sk.connect_ex(address)
  同上,只不过会有返回值,连接成功时返回 0 ,连接失败时候返回编码,例如:10061
sk.close()
  关闭套接字
sk.recv(bufsize[,flag])
  接受套接字的数据。数据以字符串形式返回,bufsize指定最多可以接收的数量。flag提供有关消息的其他信息,通常可以忽略。
sk.recvfrom(bufsize[.flag])
  与recv()类似,但返回值是(data,address)。其中data是包含接收数据的字符串,address是发送数据的套接字地址。
sk.send(string[,flag])
  将string中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于string的字节大小。即:可能未将指定内容全部发送。
sk.sendall(string[,flag])
  将string中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常。
      内部通过递归调用send,将所有内容发送出去。
sk.sendto(string[,flag],address)
  将数据发送到套接字,address是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。该函数主要用于UDP协议。
sk.settimeout(timeout)
  设置套接字操作的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如 client 连接最多等待5s )
sk.getpeername()
  返回连接套接字的远程地址。返回值通常是元组(ipaddr,port)。
sk.getsockname()
  返回套接字自己的地址。通常是一个元组(ipaddr,port)
sk.fileno()
  套接字的文件描述符

 setsockopt()

python定义了setsockopt()和getsockopt(),一个是设置选项,一个是得到设置。这里主要使用setsockopt(),具体结构如下:

setsockopt(level,optname,value)

level定义了哪个选项将被使用。通常情况下是SOL_SOCKET,意思是正在使用的socket选项。它还可以通过设置一个特殊协议号码来设置协议选项,然而对于一个给定的操作系统,大多数协议选项都是明确的,所以为了简便,它们很少用于为移动设备设计的应用程序。

optname参数提供使用的特殊选项。关于可用选项的设置,会因为操作系统的不同而有少许不同。如果level选定了SOL_SOCKET,那么一些常用的选项见下表:

选项

意义

期望值

SO_BINDTODEVICE

可以使socket只在某个特殊的网络接口(网卡)有效。也许不能是移动便携设备

一个字符串给出设备的名称或者一个空字符串返回默认值

SO_BROADCAST

允许广播地址发送和接收信息包。只对UDP有效。如何发送和接收广播信息包

布尔型整数

SO_DONTROUTE

禁止通过路由器和网关往外发送信息包。这主要是为了安全而用在以太网上UDP通信的一种方法。不管目的地址使用什么IP地址,都可以防止数据离开本地网络

布尔型整数

SO_KEEPALIVE

可以使TCP通信的信息包保持连续性。这些信息包可以在没有信息传输的时候,使通信的双方确定连接是保持的

布尔型整数

SO_OOBINLINE

可以把收到的不正常数据看成是正常的数据,也就是说会通过一个标准的对recv()的调用来接收这些数据

布尔型整数

SO_REUSEADDR

当socket关闭后,本地端用于该socket的端口号立刻就可以被重用。通常来说,只有经过系统定义一段时间后,才能被重用。

布尔型整数

 

本节在学习时,用到了SO_REUSEADDR选项,具体写法是:

S.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) 这里value设置为1,表示将SO_REUSEADDR标记为TRUE,操作系统会在服务器socket被关闭或服务器进程终止后马上释放该服务器的端口,否则操作系统会保留几分钟该端口。

setsockopt(level,optname,value)

level指定控制套接字的层次.可以取三种值:
1)SOL_SOCKET:通用套接字选项.
2)IPPROTO_IP:IP选项.
3)IPPROTO_TCP:TCP选项. 
optname指定控制的方式(选项的名称),我们下面详细解释 

optval获得或者是设置套接字选项.根据选项名称的数据类型进行转换 


选项名称        说明                  数据类型
========================================================================
            SOL_SOCKET
------------------------------------------------------------------------
SO_BROADCAST      允许发送广播数据            int
SO_DEBUG        允许调试                int
SO_DONTROUTE      不查找路由               int
SO_ERROR        获得套接字错误             int
SO_KEEPALIVE      保持连接                int
SO_LINGER        延迟关闭连接              struct linger
SO_OOBINLINE      带外数据放入正常数据流         int
SO_RCVBUF        接收缓冲区大小             int
SO_SNDBUF        发送缓冲区大小             int
SO_RCVLOWAT       接收缓冲区下限             int
SO_SNDLOWAT       发送缓冲区下限             int
SO_RCVTIMEO       接收超时                struct timeval
SO_SNDTIMEO       发送超时                struct timeval
SO_REUSERADDR      允许重用本地地址和端口         int
SO_TYPE         获得套接字类型             int
SO_BSDCOMPAT      与BSD系统兼容              int
========================================================================
            IPPROTO_IP
------------------------------------------------------------------------
IP_HDRINCL       在数据包中包含IP首部          int
IP_OPTINOS       IP首部选项               int
IP_TOS         服务类型
IP_TTL         生存时间                int
========================================================================
            IPPRO_TCP
------------------------------------------------------------------------
TCP_MAXSEG       TCP最大数据段的大小           int
TCP_NODELAY       不使用Nagle算法             int
========================================================================

shutdown()

socket.makefile

socket.makefile(mode='r', buffering=None, *, encoding=None, errors=None,newline=None), 返回一个文件对象,具体类型与参数相关,除了只能指定‘r’,‘w’,'b'模式外,与open()函数一样.

前提条件是socket必须是阻塞模式,它可以含有一个超时间(指socket),如果发生超时,内部缓冲区会在不一致的状态中关闭。

关闭文件对象不会关闭socket,除非调用了socket.close()方法,或者所有其他文件对象都关闭了。

注意:在win上,makefile创建的一个类文件对象在需要文件描述符的文件对象的情况下是无法使用的(意思大概是makefile创建的文件对象没有文件描述符)


socket.dup(), 复制一个socket

socket.detach()

,将socket 对象设置为关闭状态,但底层的文件描述符并没关闭,仍可以进行操作,返回值为文件描述符。

socket.get_inheritable()

测试socket是否可以继承,True或者False

socket.recvmsg(bufsize[, ancbufsize[, flags]])

socket.recvmsg(bufsize[, ancbufsize[, flags]]), 接收常规文件接收常规文件,附件等。ancbufsize设定附件接收缓冲区,类似于bufsize。但返回值是有四个元素的元组()。(data, ancdata, msg_flags, address). ancdata是一个含有四个0的列表或者是(cmsg_level, cmsg_type, cmsg_data)这样的列表。

scoket.share()

share(process_id) -> bytes ,给另外一个程序传递自己的二进制,别的程序可以接收,通过socket.fromshare()变成一个套接字进行通信

import socket
ip_port = ("127.0.0.1", 9999)

sk = socket.socket()             #建立一个套接字,用来接收请求
sk.bind(ip_port)                 #绑定一个端口
sk.listen(2)                     #接听,最大能接听5个

coon, addr = sk.accept()
print(coon.getpeername())                              #('127.0.0.1', 64808)
print(coon.getsockname())                              #('127.0.0.1', 9999)
# print(socket.gethostbyaddr('112.80.248.76'))           #('www.baidu.com', [], ['112.80.248.76'])
print(socket.gethostbyname("www.baidu.com"))           #112.80.248.76
print(socket.gethostbyname_ex("www.baidu.com"))        #('www.baidu.com', [], ['112.80.248.75'])
print(socket.getservbyport(80))                        #http
print(sk.get_inheritable())                            #False   实例才有这个属性
sk2 = sk.dup()
print(sk2)                                             #<socket.socket fd=712, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 9999)>

TCP

 socket模式是TCP的,常用函数 send, sendall, recv

 当发送为空的时候不再接收卡住,不知道是3.7改进了还是3.5的问题

服务端:socket()---------->绑定-------->监听listen------------>等待链接accept ----------->接收数据

客户端:socket()---------------->连接服务端connect------------>发送数据

sk.setblocking(bool)   设置True 阻塞,设置为False, 遇到阻塞accept(), recv()就报错

############服务端################
import socket
ip_port = ("127.0.0.1", 9999)                                 #当再虚拟机上运行的时候,不可以写127.0.0.1, 必须写局域网地址
BUFFERSIZE =8196


sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)             #建立一个套接字,用来接收请求
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)           #允许端口复用
sk.bind(ip_port)                 #绑定一个端口

sk.listen(2)                     #接听,最大能接听5个

while True:
    conn, addr = sk.accept()
    print("以下客户端要连接主机:",conn, addr)
    conn.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1)                   # 禁用nagle算法

    conn.sendall("你好,我是机器人1".encode("utf8"))
    conn.sendall("你好,我是机器人2".encode("utf8"))
    conn.sendall("你好,我是机器人3".encode("utf8"))              #第一次连接完不会粘包,第二三次会,可能是时间原因,如果对方睡眠一秒,则一次收到3个


    #进行某个信息处理
    while True:
        data_recv = conn.recv(BUFFERSIZE).decode("utf8")
        print("服务器收到数据:",data_recv)
        data_send = input("你要问的问题")
        conn.sendall(data_send.encode("utf8"))

    conn.close()

sk.shutdown()
sk.close()

############客户端###########
import  socket
import time

ip_port = ("127.0.0.1", 9999)
# ip_port = ("192.168.0.105", 9999)
BUFFERSIZE =8196

sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sk.connect(ip_port)                       #建立一个套接字,连接服务器端口

time.sleep(0.3)
data = sk.recv(BUFFERSIZE)
print("客户端收到》》》》:",data.decode("utf8"))

while 1:
    data = input("请问有什么问题")
    sk.sendall(data.encode("utf8"))
    data_recv = sk.recv(BUFFERSIZE)
    print("客户端收到》》》》:", data_recv.decode("utf8"))

sk.close()
聊天机器人

遇到的问题:

  1. 怎么关闭nagle, 用conn.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1),刚开始以为没弄成功,结果发现客户端接收的时候是流式接收,所以还是混在一起,然并卵
  2. 怎么让端口复用sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)    刚开始以为是复制形式,后来不知道记错了没有,还是这一个好用
#############服务器
import socket
ip_port = ("127.0.0.1", 9999)                                 #当再虚拟机上运行的时候,不可以写127.0.0.1, 必须写局域网地址
BUFFERSIZE =8196


sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)             #建立一个套接字,用来接收请求
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)           #允许端口复用
sk.bind(ip_port)                 #绑定一个端口

sk.listen(2)                     #接听,最大能接听5个


conn, addr = sk.accept()
print("以下客户端要连接主机:",conn, addr)
size  = conn.recv(1024).decode("utf8")
print("接受到的数据大小为:",size)

while True:
    conn.sendall("开始传送".encode("utf8"))
    print("让客户端开始发送")
    has_size = 0
    with open("2.jpg", "wb") as f:
        while has_size < int(size) :
            data_recv = conn.recv(1024)
            f.write(data_recv)
            has_size += len(data_recv)
    print("接收完毕", has_size, size)
    if has_size == int(size):
        conn.sendall("接收完毕".encode("utf8"))
        break

conn.close()


###############客户端
import  socket
import os
ip_port = ("127.0.0.1", 9999)

sk = socket.socket()
sk.connect(ip_port)

#先发一个文件大小
size = os.stat("1.jpg").st_size
print("要发送的文件大小为:",size)
sk.sendall(str(size).encode("utf8"))
print("发送文件大小完毕")

while True:
    data_recv = sk.recv(1024).decode("utf8")
    print("接收到服务器发送来的数据为:",data_recv)
    if data_recv == "开始传送":
        with open("1.jpg", "rb") as f :
            for line in f:
                sk.sendall(line)
    print("发送完毕")
    data_recv = sk.recv(1024).decode("utf8")
    print("等待接收是否收完:", data_recv)
    if data_recv =="接收完毕":
        break

sk.close()
发送照片

UDP

服务端:socket()------->绑定------------>等待接收数据,然后处理

客服端:socket()------->直接发

UDP发包,要确保丢包率,搞策略确保重发

#服务端
import socket
ip_port = ("127.0.0.1", 9999)                                 #当再虚拟机上运行的时候,不可以写127.0.0.1, 必须写局域网地址
BUFFERSIZE =8196


udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)             #建立一个套接字,用来接收请求
udp.bind(ip_port)                 #绑定一个端口

print("--------------")
#直接进入通信
data, addr= udp.recvfrom(1024)
print(data.decode("utf8"), addr)
data, addr= udp.recvfrom(1024)
print(data.decode("utf8"), addr)
udp.close()

#客户端

import  socket
import time

ip_port = ("127.0.0.1", 9999)
# ip_port = ("192.168.0.105", 9999)
BUFFERSIZE =8196
udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# udp.sendto(b'hello', ip_port)
udp.sendto("".encode("utf8"), ip_port)
udp.sendto(b"hello", ip_port)
udp.close()
UDP流程 

TCP和UDP包

最大包,最小包

报文封装整体结构

报文整体结构

其中以太网(Ethernet)的数据帧在链路层   
IP包在网络层   
TCP或UDP包在传输层   
TCP或UDP中的数据(Data)在应用层   
它们的关系是 数据帧{IP包{TCP或UDP包{Data}}}   

    不同的协议层对数据包有不同的称谓,在传输层叫做段(segment),在网络层叫做数据报(datagram),在链路层叫做帧(frame)。数据封装成帧后发到传输介质上,到达目的主机后每层协议再剥掉相应的首部,最后将应用层数据交给应用程序处理。

 

在应用程序中我们用到的Data的长度最大是多少,直接取决于底层的限制。   
我们从下到上分析一下:   
1.在链路层,由以太网的物理特性决定了数据帧的长度为(46+18)-(1500+18),其中的18是数据帧的头和尾,也就是说数据帧的内容最大为1500(不包括帧头和帧尾),即MTU(Maximum Transmission Unit)为1500;  
2.在网络层,因为IP包的首部要占用20字节,所以这的MTU为1500-20=1480; 
3.在传输层,对于UDP包的首部要占用8字节,所以这的MTU为1480-8=1472;   
所以,在应用层,你的Data最大长度为1472。当我们的UDP包中的数据多于MTU(1472)时,发送方的IP层需要分片fragmentation进行传输,而在接收方IP层则需要进行数据报重组,由于UDP是不可靠的传输协议,如果分片丢失导致重组失败,将导致UDP数据包被丢弃。  
从上面的分析来看,在普通的局域网环境下,UDP的数据最大为1472字节最好(避免分片重组)。   
但在网络编程中,Internet中的路由器可能有设置成不同的值(小于默认值),Internet上的标准MTU值为576,所以Internet的UDP编程时数据长度最好在576-20-8=548字节以内。
 

2、TCP、UDP数据包最大值的确定     

        UDP和TCP协议利用端口号实现多项应用同时发送和接收数据。数据通过源端口发送出去,通过目标端口接收。有的网络应用只能使用预留或注册的静态端口;而另外一些网络应用则可以使用未被注册的动态端口。因为UDP和TCP报头使用两个字节存放端口号,所以端口号的有效范围是从0到65535。动态端口的范围是从1024到65535。  

        MTU最大传输单元,这个最大传输单元实际上和链路层协议有着密切的关系,EthernetII帧的结构DMAC+SMAC+Type+Data+CRC由于以太网传输电气方面的限制,每个以太网帧都有最小的大小64Bytes最大不能超过1518Bytes,对于小于或者大于这个限制的以太网帧我们都可以视之为错误的数据帧,一般的以太网转发设备会丢弃这些数据帧。

        由于以太网EthernetII最大的数据帧是1518Bytes这样,刨去以太网帧的帧头(DMAC目的MAC地址48bits=6Bytes+SMAC源MAC地址48bits=6Bytes+Type域2Bytes)14Bytes和帧尾CRC校验部分4Bytes那么剩下承载上层协议的地方也就是Data域最大就只能有1500Bytes这个值我们就把它称之为MTU。

 

UDP 包的大小就应该是 1500 - IP头(20) - UDP头(8) = 1472(Bytes)
TCP 包的大小就应该是 1500 - IP头(20) - TCP头(20) = 1460 (Bytes)

 

注*PPPoE所谓PPPoE就是在以太网上面跑“PPP”。随着宽带接入(这种宽带接入一般为Cable Modem或者xDSL或者以太网的接入),因为以太网缺乏认证计费机制而传统运营商是通过PPP协议来对拨号等接入服务进行认证计费的,所以引入PPPoE。PPPoE导致MTU变小了以太网的MTU是1500,再减去PPP的包头包尾的开销(8Bytes),就变成1492。不过目前大多数的路由设备的MTU都为1500。

 

        如果我们定义的TCP和UDP包没有超过范围,那么我们的包在IP层就不用分包了,这样传输过程中就避免了在IP层组包发生的错误;如果超过范围,既IP数据报大于1500字节,发送方IP层就需要将数据包分成若干片,而接收方IP层就需要进行数据报的重组。更严重的是,如果使用UDP协议,当IP层组包发生错误,那么包就会被丢弃。接收方无法重组数据报,将导致丢弃整个IP数据报。UDP不保证可靠传输;但是TCP发生组包错误时,该包会被重传,保证可靠传输。

        UDP数据报的长度是指包括报头和数据部分在内的总字节数,其中报头长度固定,数据部分可变。数据报的最大长度根据操作环境的不同而各异。从理论上说,包含报头在内的数据报的最大长度为65535字节(64K)。

       我们在用Socket编程时,UDP协议要求包小于64K。TCP没有限定,TCP包头中就没有“包长度”字段,而完全依靠IP层去处理分帧。这就是为什么TCP常常被称作一种“流协议”的原因,开发者在使用TCP服务的时候,不必去关心数据包的大小,只需讲SOCKET看作一条数据流的入口,往里面放数据就是了,TCP协议本身会进行拥塞/流量控制。 

       不过鉴于Internet(非局域网)上的标准MTU值为576字节,所以建议在进行Internet的UDP编程时,最好将UDP的数据长度控制在548字节 (576-8-20)以内。

 

3、TCP、UDP数据包最小值的确定

     在用UDP局域网通信时,经常发生“Hello World”来进行测试,但是“Hello World”并不满足最小有效数据(64-46)的要求,为什么小于18个字节,对方仍然可用收到呢?因为在链路层的MAC子层中会进行数据补齐,不足18个字节的用0补齐。但当服务器在公网,客户端在内网,发生小于18个字节的数据,就会出现接收端收不到数据的情况。

       以太网EthernetII规定,以太网帧数据域部分最小为46字节,也就是以太网帧最小是6+6+2+46+4=64。除去4个字节的FCS,因此,抓包时就是60字节。当数据字段的长度小于46字节时,MAC子层就会在数据字段的后面填充以满足数据帧长不小于64字节。由于填充数据是由MAC子层负责,也就是设备驱动程序。不同的抓包程序和设备驱动程序所处的优先层次可能不同,抓包程序的优先级可能比设备驱动程序更高,也就是说,我们的抓包程序可能在设备驱动程序还没有填充不到64字节的帧的时候,抓包程序已经捕获了数据。因此不同的抓包工具抓到的数据帧的大小可能不同。下列是本人分别用wireshark和sniffer抓包的结果,对于TCP 的ACK确认帧的大小一个是54字节,一个是60字节,wireshark抓取时没有填充数据段,sniffer抓取时有填充数据段。

       

4、实际应用

        用UDP协议发送时,用sendto函数最大能发送数据的长度为:65535- IP头(20) - UDP头(8)=65507字节。用sendto函数发送数据时,如果发送数据长度大于该值,则函数会返回错误。  

        用TCP协议发送时,由于TCP是数据流协议,因此不存在包大小的限制(暂不考虑缓冲区的大小),这是指在用send函数时,数据长度参数不受限制。而实际上,所指定的这段数据并不一定会一次性发送出去,如果这段数据比较长,会被分段发送,如果比较短,可能会等待和下一次数据一起发送。

ip报头结构

ip头部结构

版本号(Version):标明了IP 协议的版本号,目前的协议版本号为4。下一代IP 协议的版本号为6。

●报文长度:指 IP 包头部长度,占4 位。

●8 位的服务类型:包括一个3 位的优先权字段(COS,Class of Service),4 位TOS 字段和1 位未用位。4 位TOS 分别代表最小时延、最大吞吐量、最高可靠性和最小费用。

●总长度:是整个IP 数据报长度,包括数据部分。

●标识符:唯一地标识主机发送的每一份数据报。通常每发送一份报文它的值就会加1。

●生存时间:设置了数据包可以经过的路由器数目。一旦经过一个路由器,TTL 值就会减1,当该字段值为0 时,数据包将被丢弃。

●协议:确定在数据包内传送的上层协议,和端口号类似,IP 协议用协议号区分上层协议。TCP 协议的协议号为6,UDP 协议的协议号为17。

●报头校验和:计算IP 头部的校验和,检查报文头部的完整性。

●源IP 地址和目的IP 地址字段:标识数据包的源端设备和目的端设备。

●IP选项:一般格式为1个字节的代码,一个字节的长度,一个字节的指针,指针的值从1开始计数,指向IP选项的内容,一般其值为4(跳过了前面的代码&长度&指针的三个字节),长度包括前面3个字节在内的整个IP选项,最大值为40。

TCP包

 这里写图片描述 1.源端口和目的端口:各占2个字节。

2.序号:占4字节。序号范围是0~2^32-1。TCP是面向字节流的,TCP连接中传送的字节流中的每个字节都按顺序编号。整个要传送的字节流的起始序号必须要在连接建立时设置。首部中的序号字段值指的是本报文段所发送的数据的第一个字节的序号。

3.确认号:4个字节,是期望收到对方下一个报文段的第一个数据字节的序号。 
若确认号=N,则表明:到序号N-1为止的所有数据都已正确收到。 
4.数据偏移:4位。指出TCP报文段的数据起始处距离报文段的起始处有多远。这个字段实际上是指出TCP报文段的首部长度。由于首部中还有长度不确定的选项字段,因此数据偏移字段是必要的。单位是32位字,也就是4字节,4位二进制最大表示15,所以数据偏移也就是TCP首部最大60字节

5.保留:6位 
下面有6个控制位说明本报文段的性质

6.紧急URG:1位 
当URG=1时,表明紧急指针字段有效。它告诉系统此报文段中有紧急数据,应尽快传送(相当于高优先级的数据),而不要按原来的排队顺序来传送。例如,已经发送了很长的一个程序在远地的主机上运行。但后来发现了一些问题,需要取消该程序的运行。因此用户从键盘发出中断命令(Control+c)。如果不使用紧急数据,那么这两个字符将存储在接收TCP的缓存末尾。只有在所有的数据被处理完毕后这两个字符才被交付接收方的应用进程。这样做就浪费了许多时间。

当URG置为1时,发送应用进程就告诉发送方的TCP有紧急数据要传送。于是发送方TCP就把紧急数据插入到本报文段数据的最前面,而在紧急数据后面的数据仍时普通数据。这时要与首部中紧急指针字段配合使用。

7.确认ACK 
仅当ACK=1时确认号字段才有效。当ACK=0时,确认号无效。TCP规定,在连接建立后所有的传送的报文段都必须把ACK置1。

8.推送PSH 
当两个应用进程进行交互式的通信时,有时在一端的应用进程希望在键入一个命令后立即就能收到对方的响应。在这种情况下,TCP就可以使用推送操作。这时,发送方TCP把PSH置1,并立即创建一个报文段发送出去。接收方TCP收到PSH=1的报文段,就尽快地交付接收应用进程,而不再等到整个缓存都填满了后向上交付。

虽然应用程序可以选择推送操作,但推送还很少使用。

9.复位RST 
tcp连接出现严重差错时释放连接,然后重新建立连接。而可以用来拒绝一个非法的报文段或拒绝打开一个连接。

当RST=1时,表明TCP连接中出现严重差错(如由于主机崩溃或其他原因),必须释放连接,然后再重新建立运输连接。RST置1还用来拒绝一个非法的报文段或拒绝打开一个连接。

10.同步SYN 
在连接建立时用来同步序号。当SYN=1而ACK=0时,表明这是一个连接请求报文段。对方若同意建立连接,则应在相应的报文段中使用SYN=1和ACK=1。因此,SYN置为1就表示这是一个连接请求或连接接受保温。

11.终止FIN 
用来释放一个连接。当FIN=1时,表明此报文段的发送方的数据已发送完毕,并要求释放运输连接。

12窗口 占2字节。窗口值是【0,2^16-1]之间的整数。窗口指的是发送本报文段的一方的接收窗口(而不是自己的发送窗口)。窗口值告诉对方: 从本报文段首部中的确认号算起,接收方目前允许对方发送的数据量。之所以要有这个限制,是因为接收方的数据缓存空间是有限的。总之,窗口值作为接收方让发送方设置其发送窗口的依据。并且窗口值是经常在动态变化着。

13.检验和:2字节。检验范围包括首部和数据两部分。和UDP用户数据报一样,在计算校验和 时,要在TCP报文段加上12字节的伪首部。

14.紧急指针:2字节。紧急指针仅在URG=1时才有意义,它指出本报文段中的紧急数据的字节数(紧急数据结束后就是普通数据)。因此,紧急指针指出了紧急数据的末尾在报文段中的位置。当所有紧急数据都处理完时,TCP就告诉应用程序恢复到正常操作。值得注意的是,即使窗口为零时也可发送紧急数据。

15.选项:长度可变,最长可达40字节。当没有使用“选项”时,TCP的首部长度是20字节。 
1)MSS 最大报文段长度 
MSS最大报文段长度(数据字段的最大长度,默认是536字节)。MSS不宜设的太大也不宜设的太小。若选择太小,极端情况下,TCP报文段只含有1字节数据,在IP层传输的数据报的开销至少有40字节(包括TCP报文段的首部和IP数据报的首部)。这样,网络的利用率就不会超过1/41。若TCP报文段非常长,那么在IP层传输时就有可能要分解成多个短数据报片。在终点要把收到的各个短数据报片装配成原来的TCP报文段。当传输出错时还要进行重传,这些也都会使开销增大。

因此MSS应尽可能大,只要在IP层传输时不需要再分片就行。在连接建立过程中,双方都把自己能够支持的MSS接入这一字段,以后就按照这个数值传送数据。 
2)窗口扩大 
窗口扩大选项是为了扩大窗口。TCP首部中窗口字段长度是16位,因此最大窗口大小就是64k字节。对于包含卫星信道的网络可能是不够用的。可以在双方初始建立TCP连接的时候就进行协商。 
3)时间戳(计算RTT,防止序号绕回) 
A. 用来计算往返时间RTT。发送方在发送报文段时把当前时钟的时间值放入时间戳字段,接收方在确认该报文段时把时间戳字段值复制到时间戳回送回答字段。因此,发送方在收到确认报文后,可以准确地计算RTT来。 
4)选择确认选项 

UDP 包

这里写图片描述 
内容:16位源端口 16位目标端口
16位的UDP包长度 UDP头部和UDP数据的总长度字节
16位头部校验和 此字段是可选项
仅提供端口号—-0-65535 1-1023注明端口 1024-65535动态端口(高端口)
客户端访问服务器时,随机在高端口中分配一个进程号;作为数据包中的源端口—用于区分客户端上的程序进程;使用注明端口来作为目标端口,用于告知所要访问的服务;

这里写图片描述 
UDP协议分为首部字段和数据字段,其中首部字段只占用8个字节,分别是个占用两个字节的源端口、目的端口、长度和检验和。

偏函数,struct

偏函数,用于固定常用函数的某些常用参数

from functools import partial
def sum(arg1, arg2):
    return arg1+arg2
if __name__ == '__main__':
    sum1 = partial(sum, 10)       #固定参数
    sum2 = partial(sum, arg2 = 1)       #固定某个参数
    print(sum1(2))
    print(sum2(2))
偏函数

 

struct 

pack(fmt,v1,v2...)<-------------------->unpack(fmt,bytes)

import struct
import ctypes
if __name__ == '__main__':
    s = struct.Struct('ii')
    s_b = s.pack(1,2)
    print(s.unpack(s_b))       #(1, 2)

    str_buf = ctypes.create_string_buffer(s.size)       #造一个缓存
    s.pack_into(str_buf, 0 , 1,2)             #把1,2按照偏移量0,存入缓存str_buf
    print(s.unpack_from(str_buf, 0))  
struct pack pack_into 使用方法

 

1.格式化对照表

2.字节顺序,大小和校准

 

socketserver

解决的问题:无论是udp还是tcp,只要不用线程,多个人聊天的时候是不能有阻塞的

socketserver流程

 socketserver聊天

TCP线程

import logging
import socketserver
import threading

DATEFMT = "%H:%M:%S"
FORMAT = "[%(asctime)s]\t [%(threadName)s,%(thread)d] %(message)s"
logging.basicConfig(level=logging.INFO, format=FORMAT, datefmt=DATEFMT)
ip_port = ("127.0.0.1", 9999)

class MyServer(socketserver.BaseRequestHandler):
    def handle(self):
        # print(self.__dict__)
        # print(self.request)
        # print(self.client_address)
        # print(self.server)
        # self.request.setsockopt(socket.SOL_SOCKET, socket.SO_RCVTIMEO, 2)
        print("已经进入到handle",self.request)
        tlist =[]
        t1 = threading.Thread(target=self.read)
        t2 = threading.Thread(target=self.write)            #不能加参数,默认是传递了一个self
        tlist.append(t1)
        tlist.append(t2)
        for i in tlist:
            i.start()
        for i in tlist:                                     #必须阻塞,否则这个线程会退出,造成self.request不存在,子线程错误
            i.join()

    def read(self):
        while True:
            # print("正在执行read",self, self.request)
            data_recv = self.request.recv(1024)
            print("客户端端传来数据:", data_recv.decode("utf8"))


    def write(self):
        while True:
            # print("正在执行write",self.request)
            # data_send = input("输入您要说的话》》》》》")
            data_send = input()
            print("%40s" % data_send)
            self.request.sendall(data_send.encode("utf8"))

if __name__ == '__main__':
    socketserver.ThreadingTCPServer.allow_reuse_address = True
    s = socketserver.ThreadingTCPServer(ip_port, MyServer, True)   #第三个bool值,为真时候表示开启绑定和监听, 为假表示只是创建一个套接字,并不绑定和监听
    # print(s.__dict__)
    s.serve_forever()
服务器端 用线程实现
import socket
import threading
import time

ip_port = ("127.0.0.1", 9999)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

s.connect(ip_port)
time.sleep(3)


def read(sock):
    while True:
        # print("正在执行read",sock)
        data_recv = sock.recv(1024)
        print("服务器端传来数据:",data_recv.decode("utf8"))


def write(sock):
    while True:
        # print("正在执行write",sock)
        # data_send = input("输入您要说的话》》》》》")
        data_send = input()
        print("%40s" % data_send)
        sock.sendall(data_send.encode("utf8"))


tlist = []
t1 = threading.Thread(target=read, args=(s, ))
t2 = threading.Thread(target=write, args=(s,))
tlist.append(t1)
tlist.append(t2)
for i in tlist:
    i.start()
for i in tlist:
    i.join()

s.close()
客户端 线程实现

注意:

  本来想用select实现,但是select对sys.stdin不支持,如果再写一个套接字,又麻烦,所以用线程实现

  暂时不支持多人聊天,主机是发送消息,一次发给客户端1,下一次是客户端2, 可以修改如下:在主线程开写程序,服务端通过“0    发送消息”来提示给第几个发送消息,需要重新修改T和readingTCPServer,在于怎么获取有几个连接管理

import socketserver
import socket
import threading
import time

ip_port = ("127.0.0.1", 9999)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

s.connect(ip_port)
time.sleep(3)


def read(sock):
    while True:
        # print("正在执行read",sock)
        data_recv = sock.recv(1024)
        print("服务器端传来数据:",data_recv.decode("utf8"))


def write(sock):
    while True:
        # print("正在执行write",sock)
        # data_send = input("输入您要说的话》》》》》")
        data_send = input(">>>")
        print("%40s" % data_send)
        sock.sendall(data_send.encode("utf8"))


tlist = []
t1 = threading.Thread(target=read, args=(s, ))
t2 = threading.Thread(target=write, args=(s,))
tlist.append(t1)
tlist.append(t2)
for i in tlist:
    i.start()
for i in tlist:
    i.join()

s.close()
TCPserver修订版 基本实现互撩的功能

 

UDP线程

#!/usr/bin/env python3
# encoding: utf-8

"""
@Version: ??
@Author: Administrator 周广露
@Contact: zhouguanglu2012@163.com
@Site: http://www.baidu.com
@Software: PyCharm
@File: stcp测试
@Created_Time: 2019/1/23 21:14
"""
import logging
import socketserver
import threading

DATEFMT = "%H:%M:%S"
FORMAT = "[%(asctime)s]\t [%(threadName)s,%(thread)d] %(message)s"
logging.basicConfig(level=logging.INFO, format=FORMAT, datefmt=DATEFMT)
ip_port = ("127.0.0.1", 9999)

class MyServer(socketserver.BaseRequestHandler):
    def handle(self):
        # print(self.client_address)
        # print(self.server)
        # print(self.__dict__)
        # self.request.setsockopt(socket.SOL_SOCKET, socket.SO_RCVTIMEO, 2)
        print("已经进入到handle",self.request)

        #添加读取和写模块
        tlist =[]
        t1 = threading.Thread(target=self.read)

        t2 = threading.Thread(target=self.write)            #不能加参数,默认是传递了一个self
        t2.setDaemon(True)                                 #让写变成守护线程和主程序死,读程序退出即可

        tlist.append(t1)
        tlist.append(t2)
        for i in tlist:
            i.start()

        for i in tlist:                                     #必须阻塞,否则这个线程会退出,造成self.request不存在,子线程错误
            i.join()

    def read(self):
        num = 1
        try :
            while True:
                if num == 1:
                    print("客户端端传来第%d数据:"% num,self.request[0].decode("utf8"))
                else:
                    data_recv, addr = self.request[1].recvfrom(1024)
                    print(data_recv, addr)
                    print("客户端端传来第%d数据:" % num, data_recv.decode("utf8"))
                num = num + 1
        except  ConnectionResetError as e:
            print(e)

    def write(self):
        while True:
            # print("正在执行write",self.request)
            # data_send = input("输入您要说的话》》》》》")
            data_send = input(">>>")
            print("%40s" % data_send)
            self.request[1].sendto(data_send.encode("utf8"), self.client_address)

if __name__ == '__main__':
    s = socketserver.ThreadingUDPServer(ip_port, MyServer)   #第三个bool值,为真时候表示开启绑定和监听, 为假表示只是创建一个套接字,并不绑定和监听
    # print(s.__dict__)
    s.serve_forever()
UDP server
#!/usr/bin/env python3
# encoding: utf-8

"""
@Version: ??
@Author: Administrator 周广露
@Contact: zhouguanglu2012@163.com
@Site: http://www.baidu.com
@Software: PyCharm
@File: stcp测试
@Created_Time: 2019/1/23 21:14
"""
import socketserver
import socket
import threading
import time

ip_port = ("127.0.0.1", 9999)
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

s.connect(ip_port)
time.sleep(3)


s.sendto(b'123', ip_port)

def read(sock):
    while True:
        # print("正在执行read",sock)
        data_recv = sock.recv(1024)
        print("服务器端传来数据:",data_recv.decode("utf8"))


def write(sock):
    while True:
        # print("正在执行write",sock)
        # data_send = input("输入您要说的话》》》》》")
        data_send = input(">>>")
        print("%40s" % data_send)
        sock.sendto(data_send.encode("utf8"), ip_port)


tlist = []
t1 = threading.Thread(target=read, args=(s, ))
t2 = threading.Thread(target=write, args=(s,))
tlist.append(t1)
tlist.append(t2)
for i in tlist:
    i.start()
for i in tlist:
    i.join()

s.close()
UDP client

注意:

   不支持多人聊天,多人连接时候,都连接到一个handle里面去了,说明UDP连接时候就开了一个连接

TCP进程和UDP进程

ForkingTCPServer在windows不存在,只有linux才有,os.fork()

问题解决

怎么解决粘包问题

在发送每个包之前加上一个struct.pack('i',文件大小),接收端每次接受一些,直到接受完,然后再decode

发送的时候14M的字符串发送需要1秒,接受第一次很快不到0.015秒,第二次1秒多

import  socket
import struct
import time

ip_port = ("127.0.0.1", 9999)                                 #当再虚拟机上运行的时候,不可以写127.0.0.1, 必须写局域网地址
BUFFERSIZE = 1024


sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)             #建立一个套接字,用来接收请求
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)           #允许端口复用
sk.bind(ip_port)                 #绑定一个端口
sk.listen(2)                     #接听,最大能接听5个

conn, addr = sk.accept()
start = time.process_time()
count = 1
while True:
    data = conn.recv(8)
    size = struct.unpack('q', data)[0]
    print(size)
    has_length = 0
    data = b''
    if size:
        while size > has_length:
            data += conn.recv(size - has_length)
            has_length = len(data)
            print("收到数据长度",has_length)
        end = time.process_time()
        # print("第%d次发送带来的是:  %s"%(count, data.decode("utf8")))
        print("用时间为:", end - start)
        count += 1
TCP服务端 多次发送 小包粘包 大包一起发都可以解决
import socket
import struct
import time


def send_one(sock, data:str):
    start = time.process_time()
    data = data.encode("utf8")
    data_length = len(data)
    data_length_bin = struct.pack('q', data_length)
    sock.sendall(data_length_bin)
    sock.sendall(data)
    end = time.process_time()
    print("发送完毕",data_length)
    print("用时间为:",end - start)


def send_more_times(data:str):
    pass


ip_port = ("127.0.0.1", 9999)
BUFFERSIZE = 1024

sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sk.connect(ip_port)  # 建立一个套接字,连接服务器端口

while True:
    send_one(sk, "吃饭了么")
    send_one(sk, "吃饭了么2")

    time.sleep(100000)
TCP客户端 多次发送 小包粘包 大包一起发都可以解决

tcp快还是udp快?

#########服务器端
import socket
import time

ip_port = ("127.0.0.1", 9999)                                 #当再虚拟机上运行的时候,不可以写127.0.0.1, 必须写局域网地址
BUFFERSIZE =8196


sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)             #建立一个套接字,用来接收请求
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)           #允许端口复用
sk.bind(ip_port)                 #绑定一个端口

sk.listen(2)                     #接听,最大能接听5个


conn, addr = sk.accept()
tc = time.process_time()
print("以下客户端要连接主机:",conn, addr)
size  = conn.recv(1024).decode("utf8")
print("接受到的数据大小为:",size)

while True:
    conn.sendall("开始传送".encode("utf8"))
    print("让客户端开始发送")
    has_size = 0
    with open("2", "wb") as f:
        while has_size < int(size) :
            data_recv = conn.recv(8192)
            f.write(data_recv)
            has_size += len(data_recv)
    print("接收完毕", has_size, size)
    if has_size == int(size):
        conn.sendall("接收完毕".encode("utf8"))
        break
tc2 = time.process_time()
print("接受完毕时间为:",tc2 - tc, has_size)
conn.close()

#########客户端
import  socket
import os
import  time
ip_port = ("127.0.0.1", 9999)

sk = socket.socket()
sk.connect(ip_port)

tc = time.process_time()
#先发一个文件大小
size = os.stat("1").st_size
print("要发送的文件大小为:",size,"发送时间为:",tc)
sk.sendall(str(size).encode("utf8"))
print("发送文件大小完毕")

i = 1
while True:
    data_recv = sk.recv(1024).decode("utf8")
    print("接收到服务器发送来的数据为:",data_recv)
    if data_recv == "开始传送":
        with open("1", "rb") as f :
            while True:
                s = f.read(8000)
                if not s:
                    break
                sk.sendall(s)
            # for line in f:
            #     if len(line) > 10000:
            #         print("接受到的长度:", len(line))
            #     sk.sendall(line)              #tcp长度超过1500的可以断开发,但是udp不行
    print("发送完毕")
    data_recv = sk.recv(1024).decode("utf8")
    print("等待接收是否收完:", data_recv)
    if data_recv =="接收完毕":
        print("第%d次传送完毕", i)
        break
    i += 1
tc2 = time.process_time()
print("发送完毕时间:",tc2- tc )
sk.close()
tcp发送文件,100M大约1秒,但是不用确保是否传错,如果用for line in f 大约18秒,所以用读取read更快
#######udp服务器
import socket
import time

ip_port = ("127.0.0.1", 9999)                                 #当再虚拟机上运行的时候,不可以写127.0.0.1, 必须写局域网地址
BUFFERSIZE =8192

udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp.bind(ip_port)  # 绑定一个端口


tc = time.process_time()
size, addr = udp.recvfrom(1024)
print("接受到的数据大小为:", size.decode("utf8"))

while True:
    udp.sendto("开始传送".encode("utf8"), addr)
    print("让客户端开始发送")
    tc = time.process_time()
    has_size = 0
    with open("2", "wb") as f:
        num = 0
        while has_size < int(size) * 0.93:
            data_recv, addr = udp.recvfrom(BUFFERSIZE)
            num += 1
            has_size += len(data_recv)
            # print("接受到部分数据:", data_recv[0:3], len(data_recv), has_size, num )
            f.write(data_recv)

    print("接收完毕", has_size, size)
    if has_size >= int(size) * 0.93 :
        udp.sendto("接收完毕".encode("utf8"), addr)
        break
tc2 = time.process_time()
print("接受完毕时间为:", tc2 - tc, has_size)

udp.close()


########udp 客户端
#!/usr/bin/env python3
# encoding: utf-8

"""
@Version: ??
@Author: Administrator 周广露
@Contact: zhouguanglu2012@163.com
@Site: http://www.baidu.com
@Software: PyCharm
@File: server
@Created_Time: 2019/1/21 22:10
"""
import os
import  socket
import time

ip_port = ("127.0.0.1", 9999)
# ip_port = ("192.168.0.105", 9999)
BUFFERSIZE =8000

udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp.connect(ip_port)

tc = time.process_time()
#先发一个文件大小
size = os.stat("1").st_size
print("要发送的文件大小为:",size,"发送时间为:",tc)
udp.sendto(str(size).encode("utf8"), ip_port)
print("发送文件大小完毕")

i = 1
while True:

    data_recv,addr  = udp.recvfrom(1024)
    tc = time.process_time()
    print("接收到服务器发送来的数据为:",data_recv.decode("utf8"),addr)
    if data_recv.decode("utf8") == "开始传送":
        num = 0
        max = 0
        with open("1", "rb") as f :
            while True:
                s = f.read(BUFFERSIZE)
                if s:
                    udp.send(s)
                    num = num + 1
                    # print("发送了:", num, num*BUFFERSIZE)
                else:
                    break
    #         has_length = 0
    #         for line in f:
    #             length = len(line)
    #             left_len = length
    #             has_len = 0
    #             if length > max:
    #                 max = length
    #             while left_len > 0 :
    #                 if left_len > BUFFERSIZE:
    #                     udp.sendall(line[has_len: has_len + BUFFERSIZE + 1])
    #                     has_length = has_length + BUFFERSIZE
    #                     # print("has_length1:",has_length)
    #                     num = num + 1
    #                     has_len = has_len + BUFFERSIZE
    #                 else:
    #                     udp.sendall(line[has_len:])
    #                     has_length = has_length + len(line[has_len:])
    #                     # print("has_length2:",has_length)
    #                     has_len = has_len + len(line[has_len:])
    #                     num = num + 1
    #                 left_len = left_len - BUFFERSIZE
    #                 has_len = has_len + BUFFERSIZE
    #                 print("发送长度:", len(line), has_length, num)
    print("发送完毕",max)
    data_recv, addr  = udp.recvfrom(BUFFERSIZE)
    print("等待接收是否收完:", data_recv.decode("utf8"))
    if data_recv.decode("utf8") =="接收完毕":
        print("第%d次传送完毕", i)
        break
    i += 1
tc2 = time.process_time()
print("发送完毕时间:",tc2- tc )
udp.close()
udp 传送文件100M大约0.6秒,但是需要确认丢包,只能保证接受95%而已

端口复用

socketserver中端口复用是设置

socketserver.ThreadingTCPServer.allow_reuse_address = True

 

设置缓冲区大小网络

TCP有自动控制网络拥塞,UDP没有控制拥塞机制

 默认缓冲区,接受和发送的默认是65536

print(sk.getsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF))      #65536
print(sk.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF))       #65536
sk.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 100000)
sk.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 100000)
默认缓冲区大小 设置缓冲区大小

 

扩展怎么自己填包,包头

利用setsockopt来设置 

什么时候用tcp,什么时候用udp?

那么如何选择tcp还是udp?

先看下人家怎么选

1 HTTP,  http协议现在已经深入影响到我们的方方面面,重要性就不说了, 它采用的是tcp 协议,为什么使用tcp, 因为它传输的内容是不可以出现丢失,乱序等各种错误的,其次它需要跨平台实现,而tcp满足了这个要求,发展到今天,http享受了tcp带来的简洁高效和跨平台,但是也承受了tcp的各种缺点,例如缺少tcp keep alive机制[这个其实是后来添加的支持,并非普遍实现], tcp协议栈的实现问题引发的难以支持海量用户并发连接[只能通过dns等级别的集群或者cdn来实现],协议太复杂导致很难模块化处理[其实这个问题已经在nginx解决了,nginx通过模块化和对协议的分段处理机制,并引入消息机制,避免了多进程[线程]的频繁切换,相比apache等老牌web服务器软件,在应对大量用户上拥有极大的优势。 即使站在今天的角度看,http也确实应该选择tcp.

2 FTP, 这个协议比http更加古老,它采用的也是tcp协议, 因为它的每一个指令,或者文件传输的数据流,都需要保证可靠性,同时要求在各种平台上广泛支持,那么就只能选择tcp, 和http不同,它采用了noop指令机制来处理tcp缺少keep alive机制带来的问题,也就是客户端必须每过一段时间,如果没有发送其他指令,就必须发送一个noop指令给服务器,避免被服务器认为是死连接。 Ftp的缺陷在哪里呢?,其次它的文件传输是采用新的数据连接来执行,等于1个用户需要2个连接,其次当一个文件正在传输的时候,你无法进行其他操作,例如列表,也许你可以把它当作是一一对应的典范,因为这样我们可以直接用命令行进行控制,但是很多用户其实是需要在下载的时候同时进行列表等操作的,为了解决这个问题,很多客户端只要开启多个指令连接[和数据连接],这样一来,无形中额外带给了Ftp服务器很多压力,而采用udp可靠传输就不存在这个问题,但是udp可靠传输是没有跨平台支持的,这样是鱼和熊掌不可兼得,对于这样一个简单的开放协议的实现,tcp是个好选择。

3 POP3/SMTP, 常见的邮件协议,没什么好说的,反应--应答模式,跨平台要求,因此tcp是个选择,这个协议的悲剧在于,当初没有考虑到邮件附件会越来越大的问题,因此它的实现中将附件文件采用了base64编码格式,用文本模式进行发送,导致产生了大量的额外流量。

4 TFTP ,这是一个非常古老的用于内部传输小文件的协议,没有FTP那么多功能,采用的是udp协议,通过在包中加入包头信息,用尽可能简单的代码来实现小文件传输,注意是小文件,是一个值得参考的udp改造应用范例.

5 通常的voip,实时视频流等,通常会采用udp协议,这是以内这些应用可以允许丢包,很多人可能认为是udp的高效率所以才在这方面有广泛应用,这我不敢苟同,我个人认为,之所以采用udp,是因为这些传输允许丢包,这是一个最大的前提

那么现在来归纳一下

[1] 如果数据要求完整,不允许任何错误发生

     [A] 应用层协议开放模式 [例如http ftp]

           建议选择tcp,几乎是唯一选择.

    [B] 应用曾协议封闭模式 [例如游戏]

         (1) 大量连接

               [a] 长连接  

                    [一] 少量数据传输

                           优先考虑可靠udp传输 , tcp建议在20000连接以下使用.

                   [二] 大流量数据传输

                          只有在10000连接以下可以考虑tcp , 其他情况优先使用udp可靠传输    

               [b] 短连接

                  [一] 少量数据传输

                         建议使用udp标准模式, 加入序列号, 如果连接上限不超2万,可以考虑tcp

                 [二] 大流量数据传输

                       10000连接以下考虑tcp ,其他情况使用udp可靠传输

               在遇到海量连接的情况下,建议优先考虑udp可靠传输,使用tcp,由于tcp/ip栈的链表实现的影响,连接越多,性能下降越快,而udp可以实现队列,是一条平滑的直线,几乎没有性能影响.

         (2) 有限连接 [通常小于2000 , 一般每服务器为几百到1000左右]

                  [a] 长连接  

                        除非有数据的实时性要求,优先考虑tcp,否则使用udp可靠传输.        

                 

               [b] 短连接

                      优先考虑tcp.

              在有限连接的情况下,使用tcp可以减少代码的复杂性,增加广泛的移植性,并且不需要考虑性能问题.  

[2] 允许丢包,甚至可以乱序

           [a] 对实时性要求比较高,例如voip , 那么udp是最优选择.

           [b] 部分数据允许丢包,部分数据要求完整,部分有实时性要求,通常这样的需求是出现在游戏里, 这时候, 基于udp协议改造的udp多路可靠传输[同时支持不可靠模式],基本是唯一能满足的,当然也可以使用tcp来传输要求完整的数据,但是通常不建议,因为同时使用tcp和udp传输数据,会导致代码臃肿复杂度增加,udp可靠传输完全可以替代tcp.

           [c]部分数据优先传输,部分可丢弃数据在规定时效内传输, 这通常是实时视频流, 在有限连接模式下,可以考虑tcp+udp , 但是通常, 可靠udp传输是最好的选择.     

总结:

  要求不丢包的、兼容性强的、链接在10000以下的就用TCP,时效性高的、链接数高的用udp 

tcp快还是udp快?

传送100M的文件tcp发送大约用时间1秒,接受也差不多1秒,是按照每次8000接的,如果改用最大发,应该更快

视频,直播用udp还是tcp,效果差异有多少?安全性能有多少?有待完善

  总结找到的内容,应该说:

  1. 网页上的视频是基于HTTP/HTTPS,传输层是TCP

  2. QQ视频聊天等是基于UDP

  3. 甚至有的应用使用p2p协议,传输层应该也是TCP

  4. 通过http进行流化视频有很多种方法

  5. 传输视频还有很多其他的应用层协议

  一方面,在网页上看视频可以忍受缓冲5s看到更清楚的视频,所以用TCP问题不大,在网络情况较好的情况下更是如此。视频聊天时绝不能容忍等待5s才听到对方的回话,所以用UDP更合适。使用TCP还是UDP是在网络条件有限的情况下对“实时性”和“传输质量”之间的权衡,不是必须用TCP或者UDP。

一次传输多少,会造成缓冲区丢包

UDP才有丢包,大约95%左右,一次缓冲区为65536,可以修改

在linux和windows里,对于空包,回撤,和直接断开连接的处理不同

对于直接回撤的空包,tcp和udp在python3.7都能直接接收

 

posted @ 2019-01-21 18:29  周sir搞人工  阅读(872)  评论(0编辑  收藏  举报