Python套接字编程(1)——socket模块与套接字编程

  在Python网络编程系列,我们主要学习以下内容:

    1. socket模块与基本套接字编程

    2. socket模块的其他网络编程功能

    3. SocketServer模块与简单并发服务器

    4. 异步编程select 和 epoll机制

    5. 常见的Python异步编程框架

    6. 协程在Python网络编程中的使用

 

  本文介绍Python下的基本套接字编程,主要基于 socket 模块,包括简单的TCP/UDP套接字编程。通过调用 socket 模块的 socket() 函数创建一个新的套接字对象,在创建套接字时需要指定新 socket 对象使用的地址族套接字类型,下文将分别予以介绍。

 

地址族(Address Family)

  地址族通常作为 socket() 函数的第一个参数, AF_UNIX 只用于类UNIX平台,如果前者没有被定义,那么该协议对应的地址族都不可用。

 socket.AF_UNIX   属于该类型的地址就是一个字符串。AF_UNIX对应的数值是:1。

 

 socket.AF_INET   IPv4地址族,(host, port) 形式的二元组,host是一个表示网络主机的字符串,port为套接字的端口号。AF_INET对应的数值是:2。

 

 socket.AF_INET6   (host, port, flowinfo, scopeid) 形式的四元组。AF_INET6对应的数值是:23。

 

套接字类型(Type)

  套接字类型用于 socket() 函数的第二个参数,但是只有 SOCK_STREAM (TCP)和 SOCK_DGRAM (UDP)是比较常见的。

 socket.SOCK_STREAM   面向流(TCP连接)的套接字,对应的数值:1。

 

 socket.SOCK_DGRAM   面向数据报(UDP连接)的套接字,对应的数值:2。

 

 socket.SOCK_RAW   对应的数值:3。

 

 socket.SOCK_RDM   对应的数值:4。

 

 socket.SOCK_SEQPACKET   对应的数值:5。

   

Socket 对象的只读属性

  Python的socket对象具有以下属性:

 socket.family   创建套接字时传入的地址族参数,访问该属性获得的是数值。

 

 socket.type   创建套接字时传入的套接字类型,访问该属性获得的是数值。

 

 socket.proto 

  

Socket对象及套接字编程

创建套接字

socket.socket([family[, type[, proto]]])

  socket() 函数创建一个新的套接字对象;

  参数:

    family——套接字对象使用的地址族,可选值:AF_INET——IPv4地址族,AF_INET6——IPv6地址族,AF_UNIX——针对类UNIX系统的套接字

    type——套接字对象的类型,可选值:SOCK_STREAM——TCP连接套接字,SOCK_DGRAM——UDP数据报套接字;

    proto——协议数通常是0,一般可以忽略该参数。

  返回:一个新的套接字对象。

*注意:

  关于地址族的问题,目前最流行的仍然是IPv4地址族,所以本文基本针对IPv4地址族展开。在Python的套接字编程中,IPv4地址是 (hostname, port) 形式的二元组,其中hostname是一个字符串,内容是网络主机的DNS域名或点分IP地址(如:'8.8.8.8');port 是一个整数,代表远端目标 socket 监听的端口。

  

查看套接字的文件描述符(fd)

socket.fileno()

  返回调用该方法的 socket 对象的 fd,fd 可以用于 select() 等机制。在Linux中一切皆文件,套接字也不例外,每个套接字都有自己的文件描述符,调用 fileno() 可以查看对应 socket 对象的描述符。

  Windows 下返回的这个值不能用于类似 os.fdopen() 这样直接操作 fd 的函数,但是在 UNIX/Linux 系统上没有这个限制。

 

绑定套接字

socket.bind(address)

  bind() 将套接字绑定到一个地址上,前提是该socket对象尚未被绑定到某个地址;

  参数:符合创建该套接字时声明的地址族格式的地址;对于AF_INET而言,如果(host, port)中的host是 "" 即空字符串,则说明允许来自一切主机的连接。

  返回值:试图绑定一个已经绑定的套接字将抛出 socket.error 。正常调用时返回值为空。

 

套接字监听连接

socket.listen(backlog)

  listen() 只由服务端 socket 调用,监听连接到该套接字上的连接。

  参数 backlog 指定该套接字可以容纳的最大连接数,至少是0;

  listen() 返回值为空。

 

阻塞等待连接

socket.accept() 

  accept() 等待并接受一个连接,能够调用该方法的套接字必须(1). 已经绑定到一个特定的地址,并且(2). 监听连接。如果没有客户端连接,则 accept() 函数阻塞直到有客户端发起连接。

  返回值: (conn, addr) 形式的二元组,其中:

     conn :一个新的套接字对象,这个套接字对象已经连接,可以用来收发消息。

     addr :连接到本套接字上来的套接字对象的网络地址,在IPv4地址族下,这是一个(host, port) 形式的二元组。

*注意:

  accept() 方法的特点在于当前服务端内核中如果没有已经完成三次握手的套接字(已建立连接队列为空),则 accept() 函数会阻塞;否则accept()函数会返回一个新的socket对象,这个套接字和服务端先前监听的套接字不同,前者称为监听套接字,而后者称为连接套接字

  在面向对象的Python中,监听套接字就是调用该方法的 socket 对象,返回的 socket 对象是连接套接字,连接套接字已经经过三次握手建立起了连接,C/S 可以通过该套接字进行通信。

  在多进程套接字编程中,往往在 server 的 accept() 成功返回时创建一个子进程,子进程和父进程的进程影像遵循“copy-on-write”规则,所以开始时是共享监听套接字连接套接字的。一般会让父进程关闭其进程内的连接套接字,而子进程关闭其进程内的监听套接字,这样保证 server 端只有一个父进程处于监听状态,同时不断孵化子进程处理到来的客户端请求。

  

client socket 发起连接

socket.connect(address)

  主动调用该方法的 socket 是客户端,连接到一个远程的 socket 对象。该函数会阻塞直到服务端接受或者拒绝客户端的连接请求;

  参数 address 是符合该套接字地址族格式的地址,对于IPv4地址族而言,;

  connect() 返回值为空。

  

从套接字中读取数据

socket.recv(bufsize[, flags])

  recv() 从套接字中接收bufsize字节的数据,返回这些数据的字符形式。对于已经连接的套接字,会一直阻塞直到数据到来或套接字断开连接。

  参数:

    bufsize -- 最大接收的数据长度,通常应该设为2的指数次;

    flags -- 默认为0,和 UNIX recv(2) 中的参数 flags 的含义相同,

  返回值:接收到的字符串数据;如果套接字断开连接,则返回空字符串;

 

socket.recv_into(buffer[, nbytes[, flags]]) 

  recv_into() 从 socket 中读取 nbytes 字节的数据写到缓存 buffer 中,而不是创建一个新的字符串;

  参数:

    buffer —— 接收读取到的数据的缓存,

    nbytes ——打算读取的字节数,如果为0或者没有指定,则会读取 buffer 能容纳的上限个字节;

    flags同 recv() 

  返回:实际读取的字节数。

 

向套接字发送数据

socket.send(string[, flags]) 

  send() 将 string 中的数据发送到套接字中,返回实际写入的字节数。如果实际写入的字节数少于len(string),那么需要重新发送剩下的字节。如果套接字的缓存中没有多余的空间,该函数会一直阻塞直到空间充裕;

  参数 string——要发送的字节序列;flags——同 recv()中的flags参数;

  返回实际发送的字节数。

  

socket.sendall(string[, flags])

  发送 string 中的全部字节,该函数一直阻塞直到完全发送。发送的套接字应该已经连接到另一个远程套接字,该方法一直发送数据直到全部发送完成,或者发送出现错误;

  参数:同 send() 方法

  如果发送成功,sendall() 返回None;否则抛出异常,不同于 send() 方法,sendall() 无法得知实际发送了多少字节。

  

关闭套接字——close()

socket.close()

  作用:关闭套接字,套接字关闭后,所有针对已关闭套接字的操作都会失败。套接字被GC的时候,会自动关闭。关闭套接字会释放连接占用的资源,但是并不一定立刻关闭连接,如果想要及时关闭连接,应该在 close() 前调用 shutdown() 。

  参数:不需要参数;

  返回值:返回值为空。

  

关闭套接字——shutdown()

socket.shutdown(how) 

  作用:关闭连接的一端或两端一起关闭;shutdown() 和 close() 的最大区别在于 shutdown() 可以选择部分关闭套接字,而 close() 默认关闭整个套接字的两端。考虑这样的情形,一方申请关闭套接字,实际上只是声明自己不会再往套接字中写数据,而套接字中此时可能还没有接收完的来自对方的数据,如果调用 close() ,则不仅己方以后不能再写数据,也不能再读数据了。如果希望部分关闭套接字,比如关闭己方的写,但保留己方的读,这样可以保证数据完全传输。

  参数 how -- 以何种方式关闭连接,可选值: socket.SHUT_RD 此后不能再读(receive); socket.SHUT_WR 此后不能再写(send); socket.SHUT_RDWR 此后不能读写。根据平台的不同,关闭连接的一端可能导致另一端也同样关闭。

   利用上面介绍的 socket 对象的方法,可以完成简单的 TCP 套接字编程。

 

TCP套接字编程

图1 TCP套接字编程中的服务端(左)与客户端(右)

  图 1 显示了TCP套接字编程的一般流程,server 只有绑定和监听后,才能阻塞在 accept() 调用上等待客户端的连接,客户端调用 connect() 后,将会与服务端通过三次握手建立TCP连接。下面的简单示例,显示了一个单进程的 TCP 回显server和client.

例1.1 单进程TCP回显server

# -*- coding: utf-8 -*-
# single_proc_tcp_server.py
import socket listen_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) listen_sock.bind(("", 4424)) listen_sock.listen(10) # terminate with Ctrl-C try: while True: conn_sock, address = listen_sock.accept() # accept() 返回新的套接字和对端的IPv4二元组地址 print("New connection from: ", address) while True: data = conn_sock.recv(1024) if not data:
break
conn_sock.sendall(data) conn_sock.close() # 关闭连接套接字 print("Connection closed from", address) finally: listen_sock.close() # 关闭监听套接字

  服务端的运行结果:

('New connection from: ', ('127.0.0.1', 64268))
('Connection closed from', ('127.0.0.1', 64268))
('New connection from: ', ('127.0.0.1', 64270))
('Connection closed from', ('127.0.0.1', 64270))
('New connection from: ', ('127.0.0.1', 64272))
('Connection closed from', ('127.0.0.1', 64272))

  

例1.2 TCP回显client

# -*- coding: utf-8 -*-
# single_proc_tcp_client.py

import socket
import time

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('127.0.0.1', 4424))

print("Connected to server.")

data = """This is a 
few lines of data 
sent from client.
"""

for line in data.splitlines():
    sock.sendall(line)
    print("Sent: ", line)
    resp = sock.recv(1024)
print("Received: ", resp) time.sleep(1)
print("Close connection to server.") sock.close()

  客户端的运行结果:

Connected to server.
('Sent: ', 'This is a ')
('Received: ', 'This is a ')
('Sent: ', 'few lines of data ')
('Received: ', 'few lines of data ')
('Sent: ', 'sent from client.')
('Received: ', 'sent from client.')
Close connection to server.

  例1.1 中的服务端是没有并发功能的,客户端中使用 sleep() 模拟比较耗时的客户端连接。同时运行多次客户端脚本,可以发现任意时刻服务端只能响应一个客户端的连接,其他的客户端将被阻塞。所以这种单进程的 server 是最简单的示例,其并不具备实际的使用意义。

 

UDP套接字编程

  下图显示了UDP套接字编程的一般流程,

图2 UDP套接字编程的一般流程

 

  与面向连接流的 TCP 协议不同,UDP 是面向无连接数据报的传输层协议,无连接的最大特点就是客户端和服务端之间不需要为通信专门建立连接,而是像把信交给邮递员那样把数据包发给UDP套接字,至于数据能否如期到达,UDP套接字是不保证的。体现在代码层面,服务端只需要绑定到具体的网络和端口即可,而客户端不需要在数据传输前通过 connect() 建立专门的连接。

  

  socket 对象与UDP套接字编程相关的方法包括:

从套接字中读取数据

socket.recvfrom(bufsize[, flags])

  作用:从套接字中接收bufsize字节的数据,同时返回发送数据的套接字的地址;

  参数:参数同 recv() 

  返回:(string, address)形式的二元组,string是接收到的数据,address则是发送这个数据的套接字的地址,具体的地址形式则取决于套接字所属的地址族。

   

socket.recvfrom_into(buffer[, nbytes[, flags]])

  recvfrom_into() 从套接字中读取 nbytes 字节的数据写到缓存 buffer 中,而不是创建一个新的字符串;

  参数:buffer —— 接收读取到的数据的缓存,nbytes ——打算读取的字节数,如果为0或者没有指定,则会读取 buffer 能容纳的上限个字节;flags同 recv() 和 recvfrom() 等函数,默认为0;

  返回: (nbytes, address) 形式的二元组,nbytes是实际写入的字节数,address是发送方套接字的地址。

  

 向套接字发送数据

socket.sendto(string, address)
socket.sendto(string, flags, address)

  sendto() 将 string 中的字节发送给 address 处的套接字,发送方不能连接到其他的套接字,也不能调用过 socket.bind() 方法,对于UDP套接字很有用,因为可以一次给很多的地址发送数据;

  参数:string——要发送的字节序列;address——目标套接字地址;flags——同以上的其他方法中的flags参数;

  返回:返回成功发送的字节数。

*注意:

  通常send()、sendall()、recv()用于 TCP socket,因为 TCP 连接建立后,两个已经连接的套接字之间通信不需要再额外指定对方的地址,而且接收来自对方的数据时也不需要额外获知对方的地址,服务端在accept() 返回时就知道客户端的地址,而客户端建立连接时就知道服务端的地址。

  sendto()、recvfrom() 则多用于 UDP 套接字,因为UDP socket不是面向连接的,因此每次发送消息都要指定这次要发送给谁,同时接收到的数据可能来自多个远端socket,可能需要在接收数据时获得发送方的地址,此时这两个方法就能派上用场。

  这种划分不是绝对的,需要根据具体需求,例如下面例2.2中的 client,由于确信只有一个通信的套接字即 server 端,因此接收数据时不关心对方的地址,就可以使用 recv()。

 

例2.1 单进程 UDP 回显 server 

#-*-encoding:utf-8-*-
# single_proc_udp_server.py

import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(("", 4424))

try:
while True:
data, address = sock.recvfrom(1024) print("Datagram from:", address) sock.sendto(data, address) finally: sock.close()

  服务端运行结果:

('Datagram from:', ('127.0.0.1', 62848))
('Datagram from:', ('127.0.0.1', 62848))
('Datagram from:', ('127.0.0.1', 62848))

  

例2.2 单进程 UDP 回显 client

#-*-encoding:utf-8-*-
# single_proc_udp_client.py

import socket
import time

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
data = """This is a 
few lines of data 
sent from client.
"""
for line in data.splitlines():
    sock.sendto(line, ('localhost', 4424))
    print("Sent:", line)
    resp = sock.recv(1024)
    print("Received: ", resp)
    time.sleep(3)
sock.close()

  客户端运行结果:

('Sent:', 'This is a ')
('Received: ', 'This is a ')
('Sent:', 'few lines of data ')
('Received: ', 'few lines of data ')
('Sent:', 'sent from client.')
('Received: ', 'sent from client.')

  UDP 套接字编程相对TCP套接字编程要简单,UDP 灵活快捷的背后牺牲了安全和可靠等诸多特性,常用在对数据安全性要求不高的多媒体传输中。

 

socket对象的其他方法

获取对端 socket 地址

socket.getpeername() 

  getpeername() 获取连接对方套接字的地址。s 必须已经连接:(1)或者是已经正确调用过 s.connetct(),(2)或者是由监听套接字的 accept() 函数生成的连接套接字

  返回值:远程连接套接字的地址,具体的形式取决于套接字的地址族。

 

 获取套接字自身的地址

socket.getsockname()

  getsockname() 返回某个套接字自己的地址,IPv4地址族下是一个 (ip, port) 形式的二元组。

  返回当前套接字的地址,具体的形式取决于套接字的地址族。

  

设置套接字是否阻塞

socket.setblocking(flag)

  作用:设置一个套接字的阻塞状态。

  参数:

    flags -- 为0时将套接字设为非阻塞,为1时将套接字设为阻塞。

  默认情况下,初始创建的套接字都是阻塞的。非阻塞状态下,如果 recv() 函数没有获取到任何数据或者 send() 函数没有立刻发出数据,都会抛出socket.error异常;而在阻塞模式下,这些调用会阻塞直到能够继续为止。 s.setblocking(0) 等价于 s.settimeout(0.0) ,而 s.setblocking(1) 等价于 s.settimeout(None) 。

  返回:成功返回None。

 

获取当前套接字的超时设置

socket.gettimeout()

  gettimeout() 返回当前套接字对象的超时秒数;

  返回值:返回当前套接字对象的 float 型超时秒数;如果当前套接字对象没有超时行为,那么返回 None 。

     套接字可能所处的状态——阻塞,非阻塞或者超时。

 

 设置套接字的超时 

socket.settimeout(value)

  settimeout() 为阻塞套接字设置超时时限(秒),

  参数:

    value——非负浮点数或None,如果指定了非负浮点数,当阻塞套接字等待达到指定的超时时限时,抛出timeout异常;如果value为None,等于关闭了套接字的超时限制,等于将套接字设为阻塞模式,此时套接字在阻塞调用上将无限等待直到事件发生。

  返回:成功调用则返回None。

 


套接字的阻塞方法

  套接字的 accept()、connect()、send()、recv()等方法都有可能导致套接字阻塞,即程序暂时无法继续获得CPU。可能的原因是没有客户端的连接、还没有和服务端建立起连接、缓冲池已满暂时无法写入等,Python为套接字定义了三种状态:未阻塞、阻塞和超时,下文将会专门探讨Python中套接字的超时机制。

 

套接字的超时(timeout)行为

  标准的 C 套接字通常只有阻塞和非阻塞两种状态,Python 套接字的超时是指通过设置套接字的 timeout 选项,当套接字调用了一个可能引起阻塞的方法,并在该方法上等待的时长超过了预先设置的阀值时,套接字不再继续等待,而是抛出异常 socket.error。超时可以用来控制套接字等待事件的上限,避免socket对象被长时间阻塞在某个调用上。

  socket 模块中和 timeout 相关的函数包括:getdefaulttimeout() 和 setdefaulttimeout()。socket 对象和timeout相关的方法包括:gettimeout() 和 settimeout()。当一个 socket 对象的timeout值为None时,说明这个socket对象是一个“阻塞型”的套接字,也就是一旦在某个方法上阻塞,套接字会一直等待直到事件发生或条件满足。


 

 

获取套接字的选项

socket.getsockopt(level, optname[, buflen]) 

  作用:返回给定套接字的某些参数;

  参数:

    level:

      SOL_SOCKET——跟套接字自身相关的选项;

      SOL_IP——跟IP协议相关的选项;

      SOL_TCP——跟TCP协议相关的选项;

      SOL_UDP——跟UDP协议相关的选项;

    optname:

      socket模块规定的一些以SO_开头的属性;

    buflen:可以为空,表示接受返回参数的缓存空间大小;如果没有设置,说明假设选项是整形的;

  返回:返回套接字选项的整型值;如果选项并不是整形的,而是一些结构体,那么传入的buflen限制了接受选项的字节串的长度,接收到字节串后,可以使用struct模块提供的工具来解包(unpacking)。

 

 设置套接字的选项

socket.setsockopt(level, optname, value)

  作用:类似于getsockopt(),但作用是为套接字设置某些选项的值;

 

从套接字创建 file 对象

socket.makefile([mode[, bufsize]]) 

  作用:创建一个Python文件对象,该对象能够从创建它的套接字里读取或写入到创建它的套接字中。创建的文件对象与套接字可以独立地关闭。套接字必须处在非阻塞状态,不能设定超时;只有当f和s都关闭时,才会真正关闭创建f的套接字。

  参数:参数mode和bufsize和内置函数file()的参数相同;

  返回:返回一个和当前套接字相关的Python文件对象

 

socket.ioctl(control, option)

 

 

socket.connect_ex(address)

 

posted @ 2015-06-22 18:11  王智愚  阅读(6086)  评论(0编辑  收藏  举报