我们了解的涉及到两个程序之间通讯的应用大致可以分为两种:
这些应用的本质是两个程序间的通信,这两个分类对应了两种架构模式:
基于socket模块(关于计算机网络基础的相关知识见 网络编程基础----计算机网络快速一览)
一个程序要在网络上找到另一个程序,需要其 ip +port(端口) (ip 定位主机,port定位应用。)
端到端 vs 点到点:
TCP(Transmission Control Protocol)
6位控制字段拎出来说一下(方便学习三握四挥):
UDP(User Datagram Protocol)
网络上各种各样的服务大多都是基于 Socket 来完成通信的
套接字有两种:
unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信
(还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET)
套接字位于网络体系中的哪一层呢?
下面图一目了然:
套接字工作流程图:
套接字工作流程梳理:
服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束
注意:
这是与编码相关问题。
1、TCP连接的建立方法
客户端在建立一个TCP连接时一般需要两步,而服务器的这个过程需要四步,具体见下面的比较。
步骤 | TCP客户端 | TCP服务器 |
第一步 | 建立socket对象 | 建立socket对象 |
第二步 | 调用connect()建立一个和服务器的连接 | 设置socket选项(可选) |
第三步 | 无 | 绑定到一个端口(也可以是一个指定的网卡) |
第四步 | 无 | 侦听连接 |
下面具体来讲这四步的建立方法:
第一步,建立socket对象:
第二步,设置和得到socket选项
python定义了setsockopt( ) 和 getsockopt( ),一个是设置选项,一个是得到设置。这里主要使用setsockopt( ),具体结构如下:
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选项,具体写法是:
这里value设置为1,表示将SO_REUSEADDR标记为TRUE,操作系统会在服务器socket被关闭或服务器进程终止后马上释放该服务器的端口,否则操作系统会保留几分钟该端口。
第三步:绑定socket
绑定即为服务器要求一个端口号。
第四步:侦听连接。
利用listen()函数进行侦听连接。该函数只有一个参数,其指明了在服务器实际处理连接的时候,允许有多少个未决(等待)的连接在队列中等待。作为一个约定,很多人设置为5。
2、简单的TCP服务器实例
建立一个简单的TCP服务器和客户端。
具体代码如下:
服务器端: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服务器建立与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段后一次发送出去,这样接收方就收到了粘包数据。
udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字 节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠
tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。
两种情况下会发生粘包
拆包的发生情况
当发送端缓冲区的长度大于网卡的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,数据不会丢失