首页 后端开发 Python教程 Socket编程实战

Socket编程实战

Nov 05, 2016 am 09:58 AM
socket

Socket 在英文中的含义为“(连接两个物品的)凹槽”,像the eye socket,意为“眼窝”,此外还有“插座”的意思。在计算机科学中,socket 通常是指一个连接的两个端点,这里的连接可以是同一机器上的,像unix domain socket,也可以是不同机器上的,像network socket。

本文着重介绍现在用的最多的 network socket,包括其在网络模型中的位置、API 的编程范式、常见错误等方面,最后用 Python 语言中的 socket API 实现几个实际的例子。Socket 中文一般翻译为“套接字”,不得不说这是个让人摸不着头脑的翻译,我也没想到啥“信达雅”的翻译,所以本文直接用其英文表述。本文中所有代码均可在 socket.py 仓库中找到。

概述

Socket 作为一种通用的技术规范,首次是由 Berkeley 大学在 1983 为 4.2BSD Unix 提供的,后来逐渐演化为 POSIX 标准。Socket API 是由操作系统提供的一个编程接口,让应用程序可以控制使用 socket 技术。Unix 哲学中有一条一切皆为文件,所以 socket 和file 的 API 使用很类似:可以进行read、write、open、close等操作。

现在的网络系统是分层的,理论上有OSI模型,工业界有TCP/IP协议簇。其对比如下:

wKioL1gcjEjiUTjaAABi5LPivLc305.gif-wh_651x-s_1283484573.gif

每层上都有其相应的协议,socket API 不属于TCP/IP协议簇,只是操作系统提供的一个用于网络编程的接口,工作在应用层与传输层之间:

                                           wKioL1gcjFqgaWDFAAAZdsywcNs717.gif

我们平常浏览网站所使用的http协议,收发邮件用的smtp与imap,都是基于 socket API 构建的。

一个 socket,包含两个必要组成部分:

地址,由 ip 与 端口组成,像192.168.0.1:80。

协议,socket 所是用的传输协议,目前有三种:TCP、UDP、raw IP。

地址与协议可以确定一个socket;一台机器上,只允许存在一个同样的socket。TCP 端口 53 的 socket 与 UDP 端口 53 的 socket 是两个不同的 socket。

根据 socket 传输数据方式的不同(使用协议不同),可以分为以下三种:

Stream sockets,也称为“面向连接”的 socket,使用 TCP 协议。实际通信前需要进行连接,传输的数据没有特定的结构,所以高层协议需要自己去界定数据的分隔符,但其优势是数据是可靠的。

Datagram sockets,也称为“无连接”的 socket,使用 UDP 协议。实际通信前不需要连接,一个优势时 UDP 的数据包自身是可分割的(self-delimiting),也就是说每个数据包就标示了数据的开始与结束,其劣势是数据不可靠。

Raw sockets,通常用在路由器或其他网络设备中,这种 socket 不经过TCP/IP协议簇中的传输层(transport layer),直接由网络层(Internet layer)通向应用层(Application layer),所以这时的数据包就不会包含 tcp 或 udp 头信息。

wKioL1gcjInhJz2PAACtwRLcJrI479.png

Python socket API

Python 里面用(ip, port)的元组来表示 socket 的地址属性,用AF_*来表示协议类型。

数据通信有两组动词可供选择:send/recv 或 read/write。read/write 方式也是 Java 采用的方式,这里不会对这种方式进行过多的解释,但是需要注意的是:

read/write 操作的具有 buffer 的“文件”,所以在进行读写后需要调用flush方法去真正发送或读取数据,否则数据会一直停留在缓冲区内。

TCP socket

TCP socket 由于在通向前需要建立连接,所以其模式较 UDP socket 负责些。具体如下:

wKioL1gcjInhJz2PAACtwRLcJrI479.png

每个API 的具体含义这里不在赘述,可以查看手册,这里给出 Python 语言的实现的 echo server。

# echo_server.py 
# coding=utf8 
import socket 
 
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 
# 设置 SO_REUSEADDR 后,可以立即使用 TIME_WAIT 状态的 socket 
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 
sock.bind(('', 5500)) 
sock.listen(5)
登录后复制
def handler(client_sock, addr): 
    print('new client from %s:%s' % addr) 
    msg = client_sock.recv(1024) 
    client_sock.send(msg) 
    client_sock.close() 
    print('client[%s:%s] socket closed' % addr) 
 
if __name__ == '__main__': 
    while 1: 
        client_sock, addr = sock.accept() 
        handler(client_sock, addr)
登录后复制
# echo_client.py 
# coding=utf8 
import socket 
 
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 
sock.connect(('', 5500)) 
sock.send('hello socket world') 
print sock.recv(1024)
登录后复制

上面简单的echo server 代码中有一点需要注意的是:server 端的 socket 设置了SO_REUSEADDR为1,目的是可以立即使用处于TIME_WAIT状态的socket,那么TIME_WAIT又是什么意思呢?后面在讲解 tcp 状态变更图时再做详细介绍。

UDP socket

wKioL1gceE-ylCauAABZfUZtBCA157.jpg-wh_651x-s_1760955589.jpg

UDP socket server 端代码在进行bind后,无需调用listen方法。

# udp_echo_server.py 
# coding=utf8 
import socket 
 
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 
# 设置 SO_REUSEADDR 后,可以立即使用 TIME_WAIT 状态的 socket 
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 
sock.bind(('', 5500)) 
# 没有调用 listen 
 
if __name__ == '__main__': 
    while 1: 
        data, addr = sock.recvfrom(1024) 
 
        print('new client from %s:%s' % addr) 
        sock.sendto(data, addr) 
 
# udp_echo_client.py 
# coding=utf8 
import socket 
 
udp_server_addr = ('', 5500) 
 
if __name__ == '__main__': 
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 
    data_to_sent = 'hello udp socket' 
    try: 
        sent = sock.sendto(data_to_sent, udp_server_addr) 
        data, server = sock.recvfrom(1024) 
        print('receive data:[%s] from %s:%s' % ((data,) + server)) 
    finally: 
        sock.close()
登录后复制

常见陷阱

忽略返回值

本文中的 echo server 示例因为篇幅限制,也忽略了返回值。网络通信是个非常复杂的问题,通常无法保障通信双方的网络状态,很有可能在发送/接收数据时失败或部分失败。所以有必要对发送/接收函数的返回值进行检查。本文中的 tcp echo client 发送数据时,正确写法应该如下:

total_send = 0 
content_length = len(data_to_sent) 
while total_send < content_length: 
    sent = sock.send(data_to_sent[total_send:]) 
    if sent == 0: 
        raise RuntimeError("socket connection broken") 
    total_send += total_send + sent
登录后复制

send/recv操作的是网络缓冲区的数据,它们不必处理传入的所有数据。

一般来说,当网络缓冲区填满时,send函数就返回了;当网络缓冲区被清空时,recv 函数就返回。

当 recv 函数返回0时,意味着对端已经关闭。

可以通过下面的方式设置缓冲区大小。

s.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, buffer_size)
登录后复制

认为 TCP 具有 framing

TCP 不提供 framing,这使得其很适合于传输数据流。这是其与 UDP 的重要区别之一。UDP 是一个面向消息的协议,能保持一条消息在发送者与接受者之间的完备性。

wKioL1gcjFqgaWDFAAAZdsywcNs717.gif

代码示例参考:framing_assumptions

TCP 的状态机

在前面echo server 的示例中,提到了TIME_WAIT状态,为了正式介绍其概念,需要了解下 TCP 从生成到结束的状态机器。(图片来源)

wKioL1gcjFqgaWDFAAAZdsywcNs717.gif

这个状图转移图非常非常关键,也比较复杂,我自己为了方便记忆,对这个图进行了拆解,仔细分析这个图,可以得出这样一个结论,连接的打开与关闭都有被动(passive)与主动(active)两种,主动关闭时,涉及到的状态转移最多,包括FIN_WAIT_1、FIN_WAIT_2、CLOSING、TIME_WAIT。

此外,由于 TCP 是可靠的传输协议,所以每次发送一个数据包后,都需要得到对方的确认(ACK),有了上面这两个知识后,再来看下面的图:

wKioL1gcjInhJz2PAACtwRLcJrI479.png

在主动关闭连接的 socket 调用 close方法的同时,会向被动关闭端发送一个 FIN

对端收到FIN后,会向主动关闭端发送ACK进行确认,这时被动关闭端处于 CLOSE_WAIT 状态

当被动关闭端调用close方法进行关闭的同时向主动关闭端发送 FIN 信号,接收到 FIN 的主动关闭端这时就处于 TIME_WAIT 状态

这时主动关闭端不会立刻转为 CLOSED 状态,而是需要等待 2MSL(max segment life,一个数据包在网络传输中最大的生命周期),以确保被动关闭端能够收到最后发出的 ACK。如果被动关闭端没有收到最后的 ACK,那么被动关闭端就会重新发送 FIN,所以处于TIME_WAIT的主动关闭端会再次发送一个 ACK 信号,这么一来(FIN来)一回(ACK),正好是两个 MSL 的时间。如果等待的时间小于 2MSL,那么新的socket就可以收到之前连接的数据。

前面 echo server 的示例也说明了,处于 TIME_WAIT 并不是说一定不能使用,可以通过设置 socket 的 SO_REUSEADDR 属性以达到不用等待 2MSL 的时间就可以复用socket 的目的,当然,这仅仅适用于测试环境,正常情况下不要修改这个属性。

实战

HTTP UA

http 协议是如今万维网的基石,可以通过 socket API 来简单模拟一个浏览器(UA)是如何解析 HTTP 协议数据的。

#coding=utf8 
import socket 
 
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 
baidu_ip = socket.gethostbyname(&#39;baidu.com&#39;) 
sock.connect((baidu_ip, 80)) 
print(&#39;connected to %s&#39; % baidu_ip) 
 
req_msg = [ 
    &#39;GET / HTTP/1.1&#39;, 
    &#39;User-Agent: curl/7.37.1&#39;, 
    &#39;Host: baidu.com&#39;, 
    &#39;Accept: */*&#39;, 
] 
delimiter = &#39;\r\n&#39; 
 
sock.send(delimiter.join(req_msg)) 
sock.send(delimiter) 
sock.send(delimiter) 
 
print(&#39;%sreceived%s&#39; % (&#39;-&#39;*20, &#39;-&#39;*20)) 
http_response = sock.recv(4096) 
print(http_response)
登录后复制

运行上面的代码可以得到下面的输出

--------------------received-------------------- 
HTTP/1.1 200 OK 
Date: Tue, 01 Nov 2016 12:16:53 GMT 
Server: Apache 
Last-Modified: Tue, 12 Jan 2010 13:48:00 GMT 
ETag: "51-47cf7e6ee8400" 
Accept-Ranges: bytes 
Content-Length: 81 
Cache-Control: max-age=86400 
Expires: Wed, 02 Nov 2016 12:16:53 GMT 
Connection: Keep-Alive 
Content-Type: text/html 
 
<html> 
<meta http-equiv="refresh" content="0;url=http://www.baidu.com/"> 
</html>
登录后复制

http_response是通过直接调用recv(4096)得到的,万一真正的返回大于这个值怎么办?我们前面知道了 TCP 协议是面向流的,它本身并不关心消息的内容,需要应用程序自己去界定消息的边界,对于应用层的 HTTP 协议来说,有几种情况,最简单的一种时通过解析返回值头部的Content-Length属性,这样就知道body的大小了,对于 HTTP 1.1版本,支持Transfer-Encoding: chunked传输,对于这种格式,这里不在展开讲解,大家只需要知道, TCP 协议本身无法区分消息体就可以了。对这块感兴趣的可以查看 CPython 核心模块 http.client

Unix_domain_socket

UDS 用于同一机器上不同进程通信的一种机制,其API适用与 network socket 很类似。只是其连接地址为本地文件而已。

代码示例参考:uds_server.py、uds_client.py

ping

ping 命令作为检测网络联通性最常用的工具,其适用的传输协议既不是TCP,也不是 UDP,而是 ICMP,利用 raw sockets,我们可以适用纯 Python 代码来实现其功能。

代码示例参考:ping.py

netstat vs ss

netstat 与 ss 是类 Unix 系统上查看 Socket 信息的命令。netstat 是比较老牌的命令,我常用的选择有

-t,只显示 tcp 连接

-u,只显示 udp 连接

-n,不用解析hostname,用 IP 显示主机,可以加快执行速度

-p,查看连接的进程信息

-l,只显示监听的连接

ss 是新兴的命令,其选项和 netstat 差不多,主要区别是能够进行过滤(通过state与exclude关键字)。

$ ss -o state time-wait -n | head 
Recv-Q Send-Q             Local Address:Port               Peer Address:Port 
0      0                 10.200.181.220:2222              10.200.180.28:12865  timer:(timewait,33sec,0) 
0      0                      127.0.0.1:45977                 127.0.0.1:3306   timer:(timewait,46sec,0) 
0      0                      127.0.0.1:45945                 127.0.0.1:3306   timer:(timewait,6.621ms,0) 
0      0                 10.200.181.220:2222              10.200.180.28:12280  timer:(timewait,12sec,0) 
0      0                 10.200.181.220:2222              10.200.180.28:35045  timer:(timewait,43sec,0) 
0      0                 10.200.181.220:2222              10.200.180.28:42675  timer:(timewait,46sec,0) 
0      0                      127.0.0.1:45949                 127.0.0.1:3306   timer:(timewait,11sec,0) 
0      0                      127.0.0.1:45954                 127.0.0.1:3306   timer:(timewait,21sec,0) 
0      0               ::ffff:127.0.0.1:3306           ::ffff:127.0.0.1:45964  timer:(timewait,31sec,0)
登录后复制

这两个命令更多用法可以参考:

SS Utility: Quick Intro

10 basic examples of linux netstat command

总结

我们的生活已经离不开网络,平时的开发也充斥着各种复杂的网络应用,从最基本的数据库,到各种分布式系统,不论其应用层怎么复杂,其底层传输数据的的协议簇是一致的。Socket 这一概念我们很少直接与其打交道,但是当我们的系统出现问题时,往往是对底层的协议认识不足造成的,希望这篇文章能对大家编程网络方面的程序有所帮助。


本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

AI Hentai Generator

AI Hentai Generator

免费生成ai无尽的。

热门文章

R.E.P.O.能量晶体解释及其做什么(黄色晶体)
2 周前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳图形设置
2 周前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您听不到任何人,如何修复音频
3 周前 By 尊渡假赌尊渡假赌尊渡假赌

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

PHP+Socket系列之IO多路复用及实现web服务器 PHP+Socket系列之IO多路复用及实现web服务器 Feb 02, 2023 pm 01:43 PM

本篇文章给大家带来了关于php+socket的相关知识,其中主要介绍了IO多路复用,以及php+socket如何实现web服务器?感兴趣的朋友下面一起来看一下,希望对大家有帮助。

Python的socket与socketserver怎么使用 Python的socket与socketserver怎么使用 May 28, 2023 pm 08:10 PM

一、基于TCP协议的socket套接字编程1、套接字工作流程先从服务器端说起。服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束,使用以下Python代码实现:importso

怎么使用Spring Boot+Vue实现Socket通知推送 怎么使用Spring Boot+Vue实现Socket通知推送 May 27, 2023 am 08:47 AM

SpringBoot端第一步,引入依赖首先我们需要引入WebSocket所需的依赖,以及处理输出格式的依赖com.alibabafastjson1.2.73org.springframework.bootspring-boot-starter-websocket第二步,创建WebSocket配置类importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Config

php socket无法连接怎么办 php socket无法连接怎么办 Nov 09, 2022 am 10:34 AM

php socket无法连接的解决办法:1、检查php是否开启socket扩展;2、打开php.ini文件,检查“php_sockets.dll”是否被加载;3、取消“php_sockets.dll”的注释状态即可。

C#中常见的网络通信和安全性问题及解决方法 C#中常见的网络通信和安全性问题及解决方法 Oct 09, 2023 pm 09:21 PM

C#中常见的网络通信和安全性问题及解决方法在当今互联网时代,网络通信已经成为了软件开发中必不可少的一部分。在C#中,我们通常会遇到一些网络通信的问题,例如数据传输的安全性、网络连接的稳定性等。本文将针对C#中常见的网络通信和安全性问题进行详细讨论,并提供相应的解决方法和代码示例。一、网络通信问题网络连接中断:网络通信过程中,可能会出现网络连接的中断,这会导致

利用PHP和Socket实现实时文件传输技术研究 利用PHP和Socket实现实时文件传输技术研究 Jun 28, 2023 am 09:11 AM

随着互联网的发展,文件传输成为人们日常工作和娱乐中不可或缺的一部分。然而,传统的文件传输方式如邮件附件或文件分享网站存在一定的限制,无法满足实时性和安全性的需求。因此,利用PHP和Socket技术实现实时文件传输成为了一种新的解决方案。本文将介绍利用PHP和Socket技术实现实时文件传输的技术原理、优点和应用场景,并通过具体案例来演示该技术的实现方法。技术

PHP+Socket系列之实现客户端与服务端数据传输 PHP+Socket系列之实现客户端与服务端数据传输 Feb 02, 2023 am 11:35 AM

本篇文章给大家带来了关于php+socket的相关知识,其中主要介绍了什么是socket?php+socket如何实现客户端与服务端数据传输?感兴趣的朋友下面一起来看一下,希望对大家有帮助。

PHP+Socket系列之实现websocket聊天室 PHP+Socket系列之实现websocket聊天室 Feb 02, 2023 pm 04:39 PM

本篇文章给大家带来了关于php+socket的相关知识,其中主要介绍了怎么使用php原生socket实现一个简易的web聊天室?感兴趣的朋友下面一起来看一下,希望对大家有帮助。

See all articles