python网络编程
套接字
套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行向文件一样打开、读写和关闭等操作。套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。网络套接字是IP地址与端口的组合。
1.0 Python与套接字
套接字的起源可以追溯到20世纪70年代,它是加利福尼亚大学的伯利克版本UNIX(称为BSD UNIX)的一部分。因此,有时你可能会听过将套接字称为伯利克套接字或BSD套接字。套接字最初是为同一主机上的应用程序所创建,使得主机上运行的一个程序(又名一个进程)与另一个运行的程序进行通信。这就是所谓的进程间通信(Inter Process Communication,IPC)。有两种类型的套接字:基于文件和面向网络的。
UNIX套接字是我们所讲的套接字的第一个家族,并且拥有一个“家族名字”AF_UNIX,它代表地址家族(address family);UNIX。包括Python在内的大多数受欢迎的平台都使用术语地址家族及其缩写AF;其他比较旧的系统可能会将地址家族表示成域(domain)或协议家族(protocol family),并使用其缩写PF而非AF。类似地,AF_LOCAL(在2000~2001年标准化)将代替AF_UNIX。然而,考虑到反向兼容性,很多系统都同时使用二者,只是对同一个常数使用不同的别名。Python本身仍在使用AF_UNIX。
因为两个进程运行在同一台计算机上,所以这些套接字都是基于文件的,这意味着文件系统支持他们的底层基础结构。这是能够说得通的,因为文件系统是一个运行在一同一主机上的多个进程之间的共享常量。
第二种类型的套接字是基于网络的,它也有自己的家族名字AF_INET,或者地址家族;因特网。另一个地址家族AF_INET6用于第6版因特网协议(IPV6)寻址。此外,还有其他的地址家族,这些要么是专业的、过时的、很少使用的。要么是仍未实现的。在所有的地址家族之中,目前AF_INET是使用得最广泛的。
Python2.5中引入了对特殊类型的Linux套接字的支持。套接字的AF_NETLINK家族(无连接)允许使用标准的BSD套接字接口进行用户级别和内核级别代码之间的IPC。之前那种解决方案比较麻烦,而这个解决方案可以将看作一种比前一种更加优雅且风险更低得解决方案,例如,添加新系统调用、/proc支持,或者对一个操作系统的“IOCTL”。
针对Linux得另一种特性(Python2.6中新增)就是支持透明的进程通信(TIPC)协议。TIPC允许计算机集群之中的机器相互通信,而无需使用基于IP的寻址方式。Python的TIPC的支持以AF_TIPC家族的方式呈现。
总的来说,Python支支持AF_UNIX、AF_ENTLINK、AF_TIPC和AF_INET家族。
1.1 套接字地址:主机-端口对
如果一个套接字象一个电话插孔——允许通信的一些基础设施,那么主机名和端口号就想区号和电话号码的组合。然而,拥有硬件和通信的能力本身并没有任何好处,除非你知道电话打给谁以及如何拨打电话。一个网络地址由主机名和端口号对组成,而这是网络通信所需要的。此外,并未事先说明必须有其他人在另一端接听;否则,你将听到这个熟悉的声音“对不起,您所拨打的电话是空号,请核对后再拨”。你可能已经在浏览网页的过程中见过一个网络类比,例如“无法连接服务器,服务器没有相应或者服务器不可达”。
有效的端口号范围为0~65535(尽管小于1024的端口号预留给了系统)。如果你正在使用POSIX兼容系统(如Linux、Mac OS X等),那么可以在/etc/services文件中找到预留端口号的列表(以及服务器/协议和套接字类型)。
1.2 面向连接的套接字与无连接的套接字
1.面向连接的套接字
不管你采用的是哪种地址家族,都有两种不同风格的套接字连接。第一种是面向连接的,这意味着在进行通信之前必须先建立一个连接,例如,使用电话系统给一个朋友打电话。这种类型的通信也称为虚拟电路或流套接字。
面向连接的通信提供序列化的、可靠的和不重复的数据交付,而没有记录边界。这基本上意味着每条消息可以拆分成多个片段,并且每一条消息片段都确保能够到达目的地,然后将它们按顺序组合在一起,最后将完整消息传递给正在等待的应用程序。
实现这种连接类型的主要协议是传输控制协议(更为人熟知的是它的缩写TCP)。为了创建TCP套接字,必须使用SOCK_STREAM作为套接字类型。TCP套接字的名字SOCK_STREAM基于流套接字的其中一种表示。因为这些套接字(AF_INET)的网络版本使用因特网协议(IP)来搜寻网络中的主机。所以整个系统通常结合这两种协议(TCP和IP)来进行(当然,也可以使用TCP和本地[非网络的AF_LOCAL/AF_UNIX]套接字,但是很明显此时并没有使用IP)。
2.无连接的套接字
与虚拟电路形成鲜明对比的是数据包类型的套接字,它是一种无连接的套接字。这意味着,在通信开始之前并不需要建立连接。此时,在数据传输过程中并无法保证它的顺序性,可靠性或重复性。然而,数据包确实保存了记录边界,这就意味着消息是以整体发送的,而并非首先分成多个片段,例如,使用面向连接的协议。
使用数据报的消息传输可以比作邮政服务。信件和包裹或许并不能以发送顺序到达。事实上,它们可能不会到达。为了将其添加到并发通信中,在网络甚至有可能存在重复的消息。
既然有那么多副作用,为什么还是用数据报呢(使用流套接字肯定有一些优势)?由于面向连接的套接字所提供的保证,因此它们的设置以及对虚拟电路连接的维护需要大量的开销。然而,数据报不需要这些开销,及它的成本更加“低廉”。因此,他们通常能提供更好的性能,并且可能是和一些类型的应用程序。
实现这种连接类型的主要协议是用户数据报协议(更为人熟知的是其缩写UDP)。为了创建UDP套接字,必须使用SOCK_DGRAM作为套接字类型。你可能知道,UDP套接字的SOCK_DGRAM名字来自于单词“datagram”(数据报)。因为这些套接字也使用因特网协议来寻找网络中的主机,所以这个系统也有一个更加普通的名字,即这两种协议(UDP和IP)的组合名字,或UDP/IP。
1.3 Python中网络编程套接字服务器
1.通用TCP服务器
创建TCP服务器的一般伪代码。
# 创建服务器套接字 ss = socket() # 套接字与地址绑定 ss.bind() # 监听链接 ss.listen() # 服务器无限循环 inf_loop: # 接受客户端连接 cs = ss.accept() # 通信循环 comm_loop: # 对话(接收/发送) cs.recv()/cs.send() # 关闭客户端套接字 cs.close() # 关闭服务器套接字(可选) ss.close()
所有套接字都是通过使用socket.socket()函数来创建。因为服务器需要占用一个端口并等待客户端的请求,所以它们必须绑定到一个本地地址。因为TCP是一种面向连接的通信系统,所以在TCP服务器开始操作之前,必须安装一些基础设施。特别地,TCP服务器必须监听(传入)的连接。一旦这个安装过程完成后,服务器就可以开始它的无限循环。
调用accept()函数之后,就开启了一个简单的(单线程)服务器,它会等待客户端的连接,默认情况下,accept()是阻塞的,这意味着执行将被暂停,直到一个连接到达。另外,套接字确实也支持非阻塞模式。一旦服务器接受了一个连接,就会返回(利用accept())一个独立的客户端套接字,用来与即将到来的消息进行交换。使用新的客户端套接字类型于将客户端的电话切换给客服代表。当一个客户电话最后接进来时,主要的总线接线员会接到这个电话,并使用另一条线路将这个电话转接给另合适的人来处理客户的需求。这将能够空出主线(原始服务套接字),以便接线员可以继续等待新的电话(客户请求),而此时客户及其连接的客服代表能够进行他们自己的对话。同样地,当一个传入的请求到达时,服务器会创建一个新的通信端口来直接与客户端进行通信,再次空出主要的端口,以使其能够接受新的客户端连接。一旦创建了临时套接字,通信就可以开始,通过使用这个新的套接字,客户端与服务器就可以开始参与发送和接收的对话中,直到连接终止。当一方关闭连接或者向对方发送一个空字符串时,通常就会关闭连接。在代码中,一个客户端连接关闭之后,服务器就会等待另一个客户端连接。最后一行代码是可选的,在这里关闭了服务器套接字。其实,这种情况永远也不会碰到,因为服务器应该在一个无线循环中运行。
2.TCP时间戳服务器
下面是一个TCP服务器程序,它接受客户端发送的数据字符串,并将其打上时间戳(格式:[时间戳]数据)并返回给客户端。
from socket import * from time import ctime HOST="" PORT=21567 BUFSIX=1024 ADDR=(HOST, PORT) tcpSerSock = socket(AF_INET, SOCK_STREAM) tcpSerSock.bind(ADDR)
# 在连接被转接或拒绝之前,传入连接请求的最大数 tcpSerSock.listen(5) while True: print("waiting for connection...") tcpCliSock, addr = tcpSerSock.accept() print("...connected from:", addr) while True: data = tcpCliSock.recv(BUFSIZ).decode() if not data: break tcpCliSock.send(('[%s] %s' % (ctime(), data).encode()) tcpCliSock.close()
# 下面这行代码其实并不需要,因为上面是一个死循环,即端口一直被监听着
tcpSerSock.close()
客户端代码
from socket import * HOST='localhost' PORT=21567 BUFSIZ=1024 ADDR=(HOST, PORT) tcpCliSock = socket(AF_INET, SOCK_STREAM) tcpCliSock.connect(ADDR) while True: data = input(">") if not data: break tcpCliSock.send(data.encode()) data = tcpCliSock.recv(BUFSIZ).decode() if not data: break print(data) tcpCliSock.close()
以上内容来自《Python核心编程 第三版》
关于socket.send()发送的数据类型的错误,本来书上是直接发送data字符串的,但是在我的电脑上一直是出现错误的情况(就是不能发送str类型的错误)
Help on method_descriptor: send(...) send(data[, flags]) -> count Send a data string to the socket. For the optional flags argument, see the Unix manual. Return the number of bytes sent; this may be less than len(data) if the network is busy.
改用bytes类型发送一次成功。测试的时候要先运行服务端监听端口,再运行客户端。
在本地运行感觉太没意思了,上云服务器试一试。
首先要开启服务器的21567端口,在防火墙的入站规则中新建名为21567的规则
因为我们通过21567端口接收/发送,所以应该相应的在出站规则中新建一个同样的规则。腾讯云服务器的话还得在服务器面板的安全组中开放端口。
完成这些设置之后,把我们的服务端脚本放到服务器上并运行它,还要把客户代码中的HOST换成服务器的公网IP,运行客户端即可发送消息。
HOST值换成服务器公网IP
测试
2. UDP时间戳服务器
UDP时间戳服务器代码
from socket import * from time import ctime HOST="" PORT=21567 BUFSIZ=1024 ADDR=(HOST, PORT) udpSerSock = socket(AF_INET, SOCK_DGRAM) udpSerSock.bind(ADDR) while True: print("waiting for message...") data, addr = udpSerSock.recvfrom(BUFSIZ) data = data.decode() udpSerSock.sendto(('[%s] %s' % (ctime(), data)).encode(), addr) print("...received from and returned to:", addr) udpCliSock.close()
UDP时间戳客户端代码
from socket import * HOST="localhost" PORT=21567 BUFSIZ=1024 ADDR=(HOST, PORT) udpCliSock = socket(AF_INET, SOCK_DGRAM) while True: data = input(">") if not data: break udpCliSock.sendto(data.encode(), ADDR) data, ADDR = udpCliSock.recvfrom(BUFSIZ) data=data.decode() if not data: break print(data.decode()) udpCliSock.close()
测试
完成。