Python网络编程----TCP\UDP、Socket、粘包

软件开发架构

我们了解的涉及到两个程序之间通讯的应用大致可以分为两种:

  1. 应用类:qq、微信、网盘、优酷这一类是属于需要安装的桌面应用
  2. web类:比如百度、知乎、博客园等使用浏览器访问就可以直接使用的应用

这些应用的本质是两个程序间的通信,这两个分类对应了两种架构模式:

  1. CS架构: client server (通过客户端访问)
  2. BS架构: browser server (通过浏览器访问)

 

如何实现相互通信 ? 

        基于socket模块(关于计算机网络基础的相关知识见 网络编程基础----计算机网络快速一览

        一个程序要在网络上找到另一个程序,需要其 ip +port(端口) (ip 定位主机,port定位应用。)

端到端 vs 点到点:

  1. 点到点是数据链路层的说法,因为数据链路层只负责直接相连的两个节点之间的通信,一个节点的数据链路层接受ip层数据并封装之后,就把数据帧从链路上发送到与其相邻的下一个节点。 
  2. 端到端是传输层的说法,因为无论tcp还是udp协议,都要负责把上层交付的数据从发送端传输到接收端,不论其中间跨越多少节点。只不过tcp比较可靠而udp不可靠而已。所以称之为端到端,也就是从发送端到接收端。
  3. 从本质上说,由物理层、数据链路层和网络层组成的通信子网为网络环境中的主机提供点到点的服务,而传输层为网络中的主机提供端到端的通信。    直接相连的节点对等实体的通信叫点到点通信。它只提供一台机器到另一台机器之间的通信,不会涉及到程序或进程的概念。同时点到点通信并不能保证数据传输的可靠性,也不能说明源主机与目的主机之间是哪两个进程在通信,这些工作都是由传输层来完成的。
  4. 端到端通信建立在点到点通信的基础之上,它是由一段段的点到点通信信道构成的,是比点到点通信更高一级的通信方式,完成应用程序(进程)之间的通信。
  5. 端到端与点到点是针对网络中传输的两端设备间的关系而言的。端到端传输指的是在数据传输前,经过各种各样的交换设备,在两端设备问建立一条链路,就象它们是直接相连的一样,链路建立后,发送端就可以发送数据,直至数据发送完毕,接收端确认接收成功。点到点系统指的是发送端把数据传给与它直接相连的设备,这台设备在合适的时候又把数据传给与之直接相连的下一台设备,通过一台一台直接相连的设备,把数据传到接收端。 
  6. 端到端传输的优点是链路建立后,发送端知道接收设备一定能收到,而且经过中间交换设备时不需要进行存储转发,因此传输延迟小。端到端传输的缺点是直到接收端收到数据为止,发送端的设备一直要参与传输。如果整个传输的延迟很长,那么对发送端的设备造成很大的浪费。端到端传输的另.一个缺点是如果接收设备关机或故障,那么端到端传输不可能实现。 
  7. 点到点传输的优点是发送端设备送出数据后,它的任务已经完成,不需要参与整个传输过程,这样不会浪费发送端设备的资源。另外,即使接收端设备关机或故障,点到点传输也可以采用存储转发技术进行缓冲。点到点传输的缺点是发送端发出数据后,不知道接收端能否收到或何时能收到数据。 
  8. 在一个网络系统的不同分层中,可能用到端到端传输,也可能用到点到点传输。如Internet网,IP及以下各层采用点到点传输,4层以上采用端到端传输。

 

传输层协议:TCP、UDP

TCP(Transmission Control Protocol)

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

6位控制字段拎出来说一下(方便学习三握四挥):

  • ACK:确认比特(Acknowledge)。当ACK=1时确认号字段才有效,代表这个封包为确认封包。ACK=0时,确认号无效。
  • PSH:(Push function)若为1时,代表要求对方立即传送缓冲区内的其他对应封包,而无需等缓冲满了才送。
  • RST:复位比特(Reset) ,当RST=1时,表明TCP连接中出现严重差错(如由于主机崩溃或其他原因),必须释放连接,然后再重新建立运输连接。
  • SYN:同步比特(Synchronous),SYN置为1,就表示这是一个连接请求或连接接受报文,通常带有 SYN 标志的封包表示『主动』要连接到对方的意思。
  • FIN:终止比特(Final),用来释放一个连接。当FIN=1时,表明此报文段的发送端的数据已发送完毕并要求释放运输连接。

UDP(User Datagram Protocol)

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

 

 

socket(套接字)是什么?

  1. 应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。Socket把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
  2. 用户无需深入理解tcp/udp协议,socket已经为我们封装好了,只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。
  3. 也有人将socket说成ip+port,ip与port的绑定就标识了互联网中独一无二的一个应用程序 ,而程序的pid是同一台机器上不同进程或者线程的标识。

 网络上各种各样的服务大多都是基于 Socket 来完成通信的

套接字有两种:

  • 基于文件类型的套接字家族:AF_UNIX

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

  • 基于网络类型的套接字家族:AF_INET

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

 

套接字位于网络体系中的哪一层呢?

下面图一目了然:

套接字工作流程图:

TCP Socket

套接字工作流程梳理

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

注意:

  • Python3中 send/recv 都是字节  python3中字符串用unicode表示(浪费资源),(bytes)utf-8/gbk会对其进行压缩(用最小的)
  • Python2中send/recv 都是字符串  python2中字符串就是(gbk or utf-8)

这是与编码相关问题。

 

 

 

基于TCP的套接字

1、TCP连接的建立方法

客户端在建立一个TCP连接时一般需要两步,而服务器的这个过程需要四步,具体见下面的比较。

步骤TCP客户端TCP服务器
第一步建立socket对象 建立socket对象
第二步调用connect()建立一个和服务器的连接设置socket选项(可选)
第三步绑定到一个端口(也可以是一个指定的网卡)
第四步侦听连接

 

下面具体来讲这四步的建立方法:

第一步,建立socket对象:

  • s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

第二步,设置和得到socket选项

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被关闭或服务器进程终止后马上释放该服务器的端口,否则操作系统会保留几分钟该端口。

 

第三步:绑定socket

绑定即为服务器要求一个端口号。

  • s.bind( (host,  port) )  #其中host为服务器ip,通常为空,也可以绑定到一个特定的ip地址。Port为端口号。

 

第四步:侦听连接。

利用listen()函数进行侦听连接。该函数只有一个参数,其指明了在服务器实际处理连接的时候,允许有多少个未决(等待)的连接在队列中等待。作为一个约定,很多人设置为5。

  • s.listen(5)


 

 

2、简单的TCP服务器实例
 
建立一个简单的TCP服务器和客户端。

  • 服务器端:TCP响应服务器,当与客户端建立连接后,服务器显示客户端ip和端口,同时将接收的客户端信息和'I get it!'传给客户端,此时等待输入一个新的信息传给客户端。
  • 客户端:TCP客户端,首先输入服务器ip地址,然后输入信息,回车后会得到服务器返回信息,然后等待服务器向其发送信息后退出。

具体代码如下:


服务器端:tcp_server.py

import socket

server = socket.socket()
server.bind(('127.0.0.1',6527)) # 绑定ip port
server.listen(5)  # 设置监听最大个数
conn,addr = server.accept()  # conn 该链接对象 addr 客户地址信息
data = conn.recv(1024)   # 接收客户信息 限定每次1024字节
response = data + b'SB'
conn.send(response)   # 发送响应数据

conn.close()  # 该连接结束
server.close()# 关闭服务

 

客户端:tcp_client.py

import socket

sk = socket.socket()
sk.connect(('127.0.0.1',6527))   # 连接服务端
name = input('please input name:')
sk.send(name.encode('utf-8'))  # 发送数据
response = sk.recv(1024)    # 接受响应

print(response.decode('utf-8'))
sk.close()  # 断连

运行结果:

please input name: scratkong
 scratkongSB

 

 

UDP服务器

UDP服务器建立与TCP相类似,具体比较如下:

步骤

UDP

TCP

第一步

建立socket对象

建立socket对象

第二步

设置socket选项

设置socket选项

第三步

绑定到一个端口

绑定到一个端口

第四步

Recvfrom()

侦听连接listen

DEMO: 法国服务器,泰国客户端。代码如下:

法国服务器端;server_udp.py

 代码如下:

import socket
udp_sk = socket.socket(type = socket.SOCK_DGRAM)
udp_sk.bind(('127.0.0.1',8888))
msg,addr = udp_sk.recvfrom(1024)
print(msg.decode('utf-8'))
data = 'bonjour'
print(data)
udp_sk.sendto(data.encode('utf8'),addr)
udp_sk.close()

 运行结果:

萨瓦迪卡   # from client
bonjour   # server 

客户端:client_udp.py

代码如下:

import socket
ip_port = ('127.0.0.1',8888)
udp_sk = socket.socket(type = socket.SOCK_DGRAM)
data = '萨瓦迪卡'
print(data)
udp_sk.sendto(data.encode('utf-8'), ip_port)
back_msg, addr= udp_sk.recvfrom(1024)
print(back_msg.decode('utf-8'),addr)

运行结果:

萨瓦迪卡  # from client
bonjour ('127.0.0.1', 8888)   # server's reply and server's address

 

 

粘包现象

只有TCP有粘包现象,UDP永远不会粘包。

首先需要掌握一个socket收发消息的原理

粘包:由于通信接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。

 

为什么出现粘包?

        发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。

        而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。

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

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

  1. TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
  2. UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
  3. tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头。

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

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

 

两种情况下会发生粘包

  1. 发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)
  2. 接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包) 

 

拆包的发生情况

当发送端缓冲区的长度大于网卡的MTU时,tcp会将这次发送的数据拆成几个数据包发送出去。

补充问题一:为何tcp是可靠传输,udp是不可靠传输

        基于tcp的数据传输请参考我的另一篇文章http://www.cnblogs.com/linhaifeng/articles/5937962.html,tcp在数据传输时,发送端先把数据发送到自己的缓存中,然后协议控制将缓存中的数据发往对端,对端返回一个ack=1,发送端则清理缓存中的数据,对端返回ack=0,则重新发送数据,所以tcp是可靠的

        而udp发送数据,对端是不会返回确认信息的,因此不可靠

补充问题二:send(字节流)和recv(1024)及sendall

        recv里指定的1024意思是从缓存里一次拿出1024个字节的数据

        send的字节流是先放入己端缓存,然后由协议控制将缓存内容发往对端,如果待发送的字节流大小大于缓存剩余空间,那么数据丢失,用sendall就会循环调用send,数据不会丢失

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值