预备知识建议:初步了解TCP.IP基础知识,Python基础知识,掌握bytes数据类型的用法。
1、TCP与UDP协议介绍
TCP/UDP 原理
TCP/UDP 位于OSI 七层模型的第4层,在IP层之前。
尽管TCP和UDP都基于IP层,UDP是无连接服务,也就是说,只是IP层通了,UDP就可以发送消息,对消息无编号。而 TCP 提供一种面向连接的、可靠的字节流服务。面向连接意味着两个使用 TCP的应用(通常是一个客户和一个服务器)在彼此交换数据之前必须先建立一个TCP连接逻辑通道。这一过程与打电话很相似,先拨号振铃,等待对方摘机说“喂”,然后才说明是谁。连接建立以后,可以持续发送消息,每个消息都会编号,如果丢失,有重发机制保障完整性。
通常,重要业务应使用TCP,不重要的实时性业务使用UDP.
在网络上的发送的TCP/IP消息,如同多层包裹
-
发送侧,是打包过程,最里面的是用户数据,先包MAC层,其次IP层,再次TCP或UDP层,最上面可能是http 层。
-
接收侧,则是解包过程,先解开 http层,再解TCP层,其次为 IP ,MAC层,最后得到了包裹里的物品,即数据。
TCP消息结构
消息到TCP这一层,会添加两部分内容, TCP首部,TCP数据。
每个TCP段都包含源端和目的端的端口号,用于寻找发端和收端应用进程。这两个值加上IP首部中的源端IP地址和目的端 IP地址唯一确定一个TCP连接。
一个IP地址和一个端口号也称为一个插口 Socket。这个术语出现在最早的 TCP规范(RFC793 )中,后来它也作为表示伯克利版的编程接口。插口对Socket Pair, 包含客户IP地址、客户端口号、服务器 IP地址和服务器端口号(也称Socket四元组), 可唯一确定网络中1条TCP连接的双方。
序号用来标识从TCP发端向TCP收端发送的数据字节流,如果将字节流看作在两个应用程序间的单向流动,则 TCP用序号对每个字节进行计数。序号是32 bit的无符号数,序号到达最大值后又从0开始。
TCP的三次握手建立连接
我们知道tcp建立连接要进行“三次握手”,即交换三个分组。大致流程如下:
• 客户端向服务器发送一个SYN J
• 服务器向客户端响应一个SYN K,并对SYN J进行确认ACK J+1
• 客户端再想服务器发一个确认ACK K+1
这个三次握手发生在socket的那几个函数中呢?请看下图:
之后,就建立了TCP连接,或者说 soket 连接建立完成, 就可以通过这个socket相互发送消息了。
在网络上传输的TCP、UDP消息,可以通过 WireShark等抓包工具捕获并解包查看(如下图),初学者可以使用这个工具对网络通信建立更直观的认识。
2. TCP Socket 编程实现步骤
对应TCP三层握手,Python Server端与客户端实现流程如下:
实现代码如下
3. UDP Socket编程实现步骤
实现代码:
UDP编程的特点:
- socket建立时,type=socket.SOCK_DGRAM,(TCP为SOCK_STREAM)
- UDP socket server 端代码在进行bind后,无须listen方法。accept 方法,
4. Socket类主要方法
Socket 对象构建方法
使用给定的地址族、套接字类型、协议编号(默认为0)来创建套接字
socket.socket([family[, type[, proto]]])
• family : AF_INET (默认ipv4), AF_INET6(ipv6) or AF_UNIX(Unix系统进程间通信).
• type : SOCK_STREAM (TCP), SOCK_DGRAM(UDP) .
• protocol : 一般为0或者默认
type 类型参数说明
Socket主要方法
Server方法:
• bind(address) 绑定主机地址与端口号
• listen(backlog) 监听
• accept 接受客户端连接请求
Client 方法
- connect() 连接服务器
服务端与客户端的公共方法
s.recv() #接收TCP数据
s.send() #发送TCP数据(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完)
s.sendall() #发送完整的TCP数据(本质就是循环调用send,sendall在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完)
s.recvfrom() #接收UDP数据,
s.sendto() #发送UDP数据
s.getpeername() #连接到当前套接字的远端的地址
s.getsockname() #当前套接字的地址
s.getsockopt() #返回指定套接字的参数
s.setsockopt() #设置指定套接字的参数
s.close() #关闭套接字
面向锁的套接字
s.setblocking() #设置套接字的阻塞与非阻塞模式
s.settimeout() #设置阻塞套接字操作的超时时间
Accept 函数
返回值为元组
(clientsocket, address) = serversocket.accept()
Send函数
s.send(string[,flag])
s为socket.socket()返回的套接字对象
string : 要发送的字符串数据
flag : 提供有关消息的其他信息,通常可以忽略
Return值是成功发送的字节数
**s.sendall(string[,flag])**
#完整发送TCP数据。将string中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。
recv函数
s.recv(bufsize[,flag])
s为socket.socket()返回的套接字对象
bufsize : 指定要接收的数据大小 ,2的整数倍,要小于系统规定的buffsize, 建议1024
flag : 提供有关消息的其他信息,通常可以忽略
返回收到的数据,是1个bytes类型的对象
Socket 其它方法
socket.SocketType()
socket.getaddrinfo()、
此函数返回1个5-tuple 的列表
(family, type, proto, canonname, sockaddr)
socket.getpeername()
获取soket连接对端地址
Socket.Setsockopt()
设置socket选项
5. Socket发送与接收文件与图片示例
Socket 发送时,只用bytes 方式发送,所以传送内容必须转换成bytes,需要做一定的处理
大文件的发送与接收
发送方:当发送大块数据时,如果超过了1条IP包允许的最大字节限制,这就要求1条数据被拆分成多次发送,
def mysend(socket, msg):
totalsent = 0
while totalsent < MSGLEN:
sent = socket.send(msg[totalsent:])
if sent == 0:
raise RuntimeError("socket connection broken")
totalsent = totalsent + sent
s.send(len(message)
mysnd(s,message)
接收方,要根据内容大小与最大窗口,循环接收。
def myreceive(self):
chunks = []
bytes_recd = 0
while bytes_recd < MSGLEN:
chunk = self.sock.recv(min(MSGLEN - bytes_recd, 2048))
if chunk == b'':
raise RuntimeError("socket connection broken")
chunks.append(chunk)
bytes_recd = bytes_recd + len(chunk)
return b''.join(chunks)
6. SocketServer处理并发请求
网络编程,1个常见场景,就是要考虑如何处理并发请求。 Python提供了SocketServer模块,简化server端的socket编程,
其中 ThreadingTCPServer子类基于多线程方式,可并行处理多个客户请求。当每个客户端请求连接到服务器时,Socket服务端都会在服务器创建一个“线程””,专门负责处理当前客户端请求,多个用户就创建多个线程。
实现步骤:
- 创建处理客户请求的handler类,从StreamRequestHandler继承,要实现handle()方法。
- 创建 socketserver.ThreadingTCPServer服务器实例,
- 启动服务端event loop.
客户端按正常的socket client实现步骤编程即可。
实现代码示例
import socketserver
import threading
ServerAddress = ("127.0.0.1", 6060)
class MyTCPClientHandler(socketserver.StreamRequestHandler):
def handle(self):
# Receive and print the data received from client
print("Recieved one request from {}".format(self.client_address[0]))
msg = self.rfile.readline().strip()
print("Data Recieved from client is:".format(msg))
print(msg)
print("Thread Name:{}".format(threading.current_thread().name))
# Create a Server Instance
TCPServerInstance = socketserver.ThreadingTCPServer(ServerAddress, MyTCPClientHandler)
# Make the server wait forever serving connections
TCPServerInstance.serve_forever()
客户端为测试多个清求,在本地用多线程发起多个连接请求至服务器。
import socket
import threading
def Connect2Server():
#Create a socket instance
socketObject = socket.socket()
#Using the socket connect to a server...in this case localhost
socketObject.connect(("localhost", 6060))
print("Connected to localhost")
# Send a message to the web server to supply a page as given by Host param of GET request
HTTPMessage = "GET / HTTP/1.1\r\nHost: localhost\r\n Connection: close\r\n\r\n"
bytes = str.encode(HTTPMessage)
socketObject.sendall(bytes)
# Receive the data
while(True):
data = socketObject.recv(1024)
print(data)
if(data==b''):
print("Connection closed")
break
socketObject.close()
print("Client - Main thread started")
ThreadList = []
ThreadCount = 20
for index in range(ThreadCount):
ThreadInstance = threading.Thread(target=Connect2Server())
ThreadList.append(ThreadInstance)
ThreadInstance.start()
# Main thread to wait till all connection threads are complete
for index in range(ThreadCount):
ThreadList[index].join()
7. Socket编程注意事项
Socket 编程相当灵活,开发者需要处理好数据编码,接收窗口大小,超时或失败重传等事项。 在实际应用socket方式通信时,请求-响应的消息对及内容,应该尽可能简洁,消息类别要少,内容变化少,以减少出错机率。
对于复杂的通信场景,不稳定的网络,非局域网环境等场景,建议:
1、使用成熟的第3方soket库,如 PyZMQ, Twisted库,对socket 进行封装装,简化了编程,增强了网络容错处理能力。
2、使用高层协议,如基于http的 Websocket,来代替TCP编程,当然,多了几层协议,带宽开销也增加了。