当前位置: 首页 > 科技观察

Socket编程实践

时间:2023-03-13 17:11:09 科技观察

Socket在英文中的意思是“凹槽(连接两个物品)”,和eyesocket一样,是“眼窝”的意思,也有“插座”的意思。在计算机科学中,套接字通常指连接的两个端点。这里的连接可以在同一台机器上,如unix域套接字,也可以在不同的机器上,如网络套接字。本文重点介绍最常用的网络套接字,包括其在网络模型中的地位、API编程范式、常见错误等,最后给出几个使用Python语言的套接字API实现的实际例子。Socket中文一般译为“套接字”。不得不说这是一个令人费解的翻译。没想到还有“xindaya”的翻译,所以这篇文章直接用英文表达。本文中的所有代码都可以在socket.py存储库中找到。概述Socket作为一个通用的技术规范,最初是伯克利大学于1983年为4.2BSDUnix提供的,后来逐渐演变为POSIX标准。SocketAPI是操作系统提供的编程接口,允许应用程序控制套接字技术的使用。在Unix哲学中,万物皆文件,所以socket和file的API用法非常相似:可以进行read、write、open、close等操作。目前的网络体系是分层的。理论上有OSI模型,工业上有TCP/IP协议簇。对比如下:每一层都有对应的协议,socketAPI不属于TCP/IP协议簇,而是操作系统提供的网络编程接口,工作在应用层和传输层之间:我们通常browse网站使用的http协议,用于收发邮件的smtp和imap都是基于socketAPI构建的。一个套接字包含两个必要的组成部分:地址,由ip和端口组成,如192.168.0.1:80。Protocol,socket使用的传输协议,目前有三种:TCP、UDP、rawIP。地址和协议可以确定一个套接字;一台机器上只允许存在一个相同的套接字。TCP53端口的套接字和UDP53端口的套接字是两个不同的套接字。根据socket传输数据的方式不同(使用不同的协议),可分为以下三种:Streamsockets,也称为“面向连接”的sockets,使用TCP协议。实际通信前需要连接,传输的数据没有特定的结构,所以高层协议需要自己定义数据分隔符,但它的优点是数据可靠。数据报套接字,也称为“无连接”套接字,使用UDP协议。在实际通信之前不需要连接。一个优点是UDP数据包本身是自定界的,也就是说每个数据包都标记了数据的开始和结束。缺点是数据不可靠。原始套接字通常用于路由器或其他网络设备。这种socket不经过TCP/IP协议簇中的传输层(transportlayer),直接从网际层(Internetlayer)通向应用层(Applicationlayer),所以此时,数据数据包将不包含tcp或udp标头信息。在PythonsocketAPIPython中,使用(ip,port)的元组表示socket的地址属性,使用AF_*表示协议类型。数据通信有两组动词可供选择:send/recv或read/write。read/write方式也被Java采用。此处对该方法不做过多解释,但需要注意的是:读写操作是一个带有缓冲区的“文件”,因此读写后需要调用flush。方法实际发送或读取数据,否则数据将保留在缓冲区中。TCPsocketTCPsocket比UDPsocket负责,因为它需要在连接前建立连接。具体如下:各个API的具体含义这里不再赘述,可以查看手册,这里是用Python语言实现的echoserver。#echo_server.py#coding=utf8importsocketsock=socket.socket(socket.AF_INET,socket.SOCK_STREAM)#设置SO_REUSEADDR后,socketsock.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)sock.bind(('',5500))sock.listen(5)defhandler(client_sock,addr):print('newclientfrom%s:%s'%addr)msg=client_sock.recv(1024)client_sock.send(msg)client_sock.close()print('客户端[%s:%s]socketclosed'%addr)if__name__=='__main__':while1:client_sock,addr=sock.accept()handler(client_sock,addr)#echo_client.py#coding=utf8importsocketsock=socket.socket(socket.AF_INET,socket.SOCK_STREAM)sock.connect(('',5500))sock.send('hellosocketworld')printsock.recv(1024)上面简单的echoserver代码有一点需要注意:SO_REUSEADDR是为服务器端的套接字设置为1。目的是立即使用处于TIME_WAIT状态的socket,那么TIME_WAIT是什么意思呢?后面在讲解tcp状态变化图的时候会详细介绍。UDPsocketUDPsocket服务端代码绑定后不需要调用listen方法。#udp_echo_server.py#coding=utf8importsocketsock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)#设置SO_REUSEADDR后,socketsock.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)sock.bind(('',5500))#Nocalllistenif__name__=='__main__':while1:data,addr=sock.recvfrom(1024)print('newclientfrom%s:%s'%addr)sock.sendto(data,addr)#udp_echo_client.py#coding=utf8importsocketudp_server_addr=('',5500)if__name__=='__main__':sock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)data_to_sent='helloudpsocket'try:sent=sock.sendto(data_to_sent,udp_server_addr)data,server=sock.recvfrom(1024)print('receivedata:[%s]from%s:%s'%((data,)+server))finally:sock.close()Commontrapignorereturn这个echoserver的例子由于篇幅限制,文章也忽略了返回值。网络通信是一个非常复杂的问题,通常无法保证通信双方的网络状态,发送/接收数据时极有可能失败或部分失败。所以有必要检查发送/接收函数的返回值。本文tcpechoclient发送数据时,正确的写法应该是:total_send=0content_length=len(data_to_sent)whiletotal_sendhttp_response是直接调用recv(4096)得到的。如果实际收益大于这个值怎么办?我们知道TCP协议是面向流的,它并不关心报文的内容。由应用程序来定义消息的边界。对于应用层的HTTP协议,有几种情况。最简单的就是通过解析返回值的header的Content-Length属性来知道body的大小。对于HTTP1.1版本,支持Transfer-Encoding:chunked传输。对于这种格式,我这里就不解释了。你只需要知道TCP协议本身是无法区分消息体的。如果对此感兴趣,可以查看CPython核心模块http.clientUnix_domain_socketUDS是一种在同一台机器上不同进程之间进行通信的机制,其API与网络套接字非常相似。只是它的连接地址是一个本地文件。代码示例参考:uds_server.py、uds_client.pyping命令是最常用的检测网络连通性的工具,其适用的传输协议既不是TCP也不是UDP,而是ICMP。使用原始套接字,我们可以应用纯Python代码来实现其功能。代码示例参考:ping.pynetstatvsssnetstat和ss是类Unix系统上查看Socket信息的命令。netstat是一个比较老式的命令。我经常选择-t,只显示tcp连接-u,只显示udp连接-n,不解析主机名,用IP显示主机,可以加快执行速度。-p,查看连接的进程信息-l,只显示被监控的连接ss是一个新的命令,它的选项和netstat类似,主要区别是可以过滤(通过state和exclude关键字)。$ss-ostatetime-wait-n|headRecv-QSend-QLocalAddress:PortPeerAddress:Port0010.200.181.220:222210.200.180.28:12865timer:(timewait,33sec,0)00127.0.0.1:45977127.0.0.1:33.06timer12,0)0)0.1:45945127.0.0.0.1:3306Timer:(时间任务,6.621ms,0)0010.200.181.220:222210.200.200.180.28:12280Timer:12sec,timewait,12sec,0)0010.200.181.220:222210.200.180.28:42675timer:(timewait,46sec,0)00127.0.0.1:45949127.0.0.1:3306timer:(timewait,11sec,0)001267.0.0.01:4593:4595:timewait0)ffff:127.0.0.1:3306::ffff:127.0.0.1:45964timer:(timewait,31sec,0)这两个命令的更多用法请参考:SSUtility:QuickIntro10basicExamplesoflinuxnetstatcommandSummaryOur生活离不开网络,平时的开发也充斥着各种复杂的网络应用,从最基础的数据库到各种分布式系统,再复杂的应用层,底层的传输协议族ng数据一致。我们很少直接和Socket这个概念打交道,但是当我们的系统出现问题的时候,往往是对底层协议的理解不够导致的。希望这篇文章可以帮助到你编写网络程序。