半开放socket

通常socket是双向的,有时你会想建立一个单向的socket,单向的socket被称为半开放socket。为此,需要调用shutdown()函数,对于socket,这个操作是不可逆的。半开放socket可以用于一下情况:

你想要确保所有写好的数据都已经被传送出去。当为了关闭socket的输出频道而调用shutdown()函数的时候,只有在缓存里面的数据都被成功发送出去后,才会有返回。

你想有个办法来捕获潜在的程序错误,这些错误是由数图写一个不能写的socket,或者读一个不该读的socket引起的。

你的程序使用了fork()或多线程,而你想防止其他进程或线程的某些操作,或者你像立刻关闭一个socket。


使用shutdown()的一个原因是帮助你确保代码的正确。例如,若你结束了写操作并使用了shutdown()来防止将来再进行写操作,一旦你将来试图再进行写操作,你将得到一个异常。通常捕获异常要比跟踪死锁或协议错误容易。

当程序使用fork()或多线程的时候,还会产生一种情况。若使用fork(),对socket的close()函数调用,只能保证对那个特别的进程来说连接是关闭的。只有所有的进程或者调用了close(),或者socket超过范围,或者socket被删除和终止,该连接才会真正地关闭。可以通过调用shutdown()来终止双向通信,进而关闭socket。

shutdown()的参数:0 表示禁止将来读;1 表示禁止将来写;2 表示禁止将来读和写


超时

超时在很多情况下对发现错误或连接问题很有用。一些用于主动连接中的程序,如果没有任何活动超过了一分钟,该程序就会收到警告。而不是等待来自操作系统关于失败的通知。在读数据的时候,超时可以用于强迫断开不活动的客户端。无论是读还是写数据,都可以用超时来检查断掉的连接。

为了让Python的socket具有超时检查功能,需要调用socket的settimeout()函数,向传递给它的参数表明,经过多少秒数就算是超时。稍后,当访问一个socket,如果经过参数设定的一定的时间后,什么都没有发生,就会产生一个socket.timeout异常。例子:

#!/usr/bin/env python

from socket import *
import traceback

HOST=''
PORT=51234
ADDR=(HOST,PORT)

s=socket(AF_INET,SOCK_STREAM)
s.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
s.bind(ADDR)
s.listen(1)

while True:
    try:
        client,addr=s.accept()
    except KeyboardInterrupt:
        raise
    except:
        traceback.print_exc()
        continue

    client.settimeout(5)

    try:
        print 'Got connection from :',client.getpeername()
        while True:
            data=client.recv(4096)
            if not len(data):
                break
            client.sendall(data)
    except (KeyboardInterrupt,SystemExit):
        raise
    except timeout:
        print 'Time out'
    except:
        traceback.print_exc()

    try:
        client.close()
    except KeyboardInterrupt:
        raise
    except:
        traceback.print_exc()

当使用socket超时时,需要特别注意的是读和写操作都会产生超时。对于读操作,如果客户端没有发送任何数据,就可能会产生超时。对于写操作,客户端也许还没有开始试图读取数据。在这两种情况下,网络拥挤或出现问题都会引起超时。这就是在这个例子中,捕获socket.timeout的语句覆盖了recv()和sendall()的调用。


传输字符串

在网络传输数据有一个普遍的问题,就是传输不确定长度的字符串。不知道什么时候数据发送结束。有两种方法可以解决这个问题:一个唯一的字符串结束标识符和一个在开始部分的固定长度字符串大小的指示器。

在第一种方法中,发送方会在发送正文后附加一个字符串结束标识符。这个标识符是一个NULL字符或是换行符。若使用换行符,就能方便地使用文件类对象的方法readline()来得到数据。但是,换行符也经常用在正文中,所以若一小块单独的数据却有很多行,就有问题。这时可以用NULL字符。但是,若字符串中包含二进制数据,它可能会包含NULL字符。这种情况下,没有可用的唯一字符串结束标识符。这时可以用转义符,数据编码,或是直接用第二种方法。

第二种方法中,首先发送一个固定宽度的数字,表示要发送数据的长度,接收方会根据这个数字接受该长度的数据。


理解网络字节顺序

在网络上发送整型数据的时候,有两种选择:

一个ASCII码的字符串,接收方需解析;

一个二进制字,一般是16或32位长

在发送一个二进制整数之前,该整数被转换成网络字节顺序。接收方收到后,在使用该数据之前,会先把网络字节顺序转换成本地的表示方法。Python的struct模块提供了把数据在Python和二进制数据之间转换的支持。struct模块是以一个格式化字符串为基础。在这里主要使用两种基本的格式:H,适用于16位整数;I,适用于32位的整数。惊叹号表示struct模块使用网络字节顺序来进行编码和解码。例子:

#!/usr/bin/env python

import struct,sys

def htons(num):
    return struct.pack('!H',num)

def htonl(num):
    return struct.pack('!I',num)

def ntohs(data):
    return struct.unpack('!H',data)[0]

def ntohl(data):
    return struct.unpack('!I',data)[0]

def sendstring(data):
    return htonl(len(data))+data

print 'Enter a string:'
str=sys.stdin.readline().rstrip()

print repr(sendstring(str))

在网络上传输二进制字时,有一种标准的二进制数据表示法,称为网络字节顺序。在发送一个整数前,会被转换成网络字节顺序。接收方收到后,会先把网络字节顺序转换成本地的表示方法。struct模块提供了把数据在Python和二进制数据之间转换的支持。此模块支持很多类型的字符。主要用的有:H:适用于无符号短整型(16位),I:无符号整型(32位),c:字符


使用广播数据

广播数据不能用TCP实现,多数是用UDP来实现的。发送方广播一个UDP信息包给局域网上的所有机器。在接收方,当收到一个广播信息包后,检查目的地和端口号,若正有一个进程在侦听该端口,则信息包会发送给该进程,否则就丢弃。

为了使socket支持广播,必须首先调用setsockopt(SOL_SOCKET,SO_BROADCAST,1)。要发送一个广播,可以使用特殊的地址“<broadcast>”。在广播中,发送方和接收方都要进行配置。例子:

接收方(broadcast server):

#!/usr/bin/env python

from socket import *
import traceback

HOST=''
PORT=51234
ADDR=(HOST,PORT)

s=socket(AF_INET,SOCK_DGRAM)
s.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
s.setsockopt(SOL_SOCKET,SO_BROADCAST,1)
s.bind(ADDR)

while True:
    try:
        message,addr=s.recvfrom(8192)
        print 'Got data from',addr
        s.sendto('I am here',addr)
    except (KeyboardInterrupt,SystemExit):
        raise
    except:
        traceback.print_exc()

发送方(broadcast sender):

#!/usr/bin/env python

from socket import *
import sys
dest=('<broadcast>',51234)
s=socket(AF_INET,SOCK_DGRAM)
s.setsockopt(SOL_SOCKET,SO_BROADCAST,1)
s.sendto('Hello',dest)
print 'Looking for replies'
while True:
    (buf,addr)=s.recvfrom(2048)
    if not len(buf):
        break
    print 'Received from %s:%s'%(addr,buf)

关于广播需要注意的地方是尽量少用广播。


IPv6

家族AF_INET6。socket.getaddrinfo()函数可以使用一种或两种协议来查找地址。


使用poll()或select()实现事件通知

通常情况下,socket上的I/O会阻塞。也就是说,除非一个操作结束,否则程序是不会继续执行的。select()和poll()可以通知操作系统是哪个socket对你的程序“感兴趣”。当某个socket上有事件发生的时候,操作系统会通知你发生了什么,这样就可以进行处理。select()是早期接口,在同时观察多个socket的时候比较笨重。。下面是一个使用poll()的例子:

服务器程序(每5秒向客户端发送一段文本的一行):

#!/usr/bin/env python

from socket import *
import traceback,time

HOST=''
PORT=51234
ADDR=(HOST,PORT)

s=socket(AF_INET,SOCK_STREAM)
s.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
s.bind(ADDR)
s.listen(1)

while True:
    try:
        client,addr=s.accept()
    except KeyboardInterrupt:
        raise
    except:
        traceback.print_exc()
        continue

    try:
        print "Got connection from",client.getpeername()
        while True:
            try:
                client.sendall(time.asctime()+'\n')
            except:
                break
            time.sleep(5)
    except (KeyboardInterrupt,SystemExit):
        raise
    except:
        traceback.print_exc()

try:
    client.close()
except KeyboardInterrupt:
    raise
except:
    traceback.print_exc()

客户端程序:

#!/usr/bin/env python

from socket import *
import sys,select

HOST='localhost'
PORT=51234
ADDR=(HOST,PORT)

spinsize=10
spinpos=0
spindir=1

def spin():
    global spinsize,spinpos,spindir
    spinstr='.'*spinpos+'|'+'.'*(spinsize-spinpos-1)
    sys.stdout.write('\r'+spinstr+' ')
    sys.stdout.flush()

    spinpos+=spindir
    if spinpos<0:
        spindir=1
        spinpos=1
    elif spinpos>=spinsize:
        spinpos-=2
        spindir=-1

s=socket(AF_INET,SOCK_STREAM)
s.connect(ADDR)
p=select.poll()
p.register(s.fileno(),select.POLLIN|select.POLLERR|select.POLLHUP)#对这些事情使用poll,对到来的数据感兴趣

while True:
    results=p.poll(50)#表示等待50毫秒后可能会发生某件事情,若什么都没有发生,则p.poll()返回一个空的列表
    if len(results):
        if results[0][1]==select.POLLIN:
            data=s.recv(4096)
            if not len(data):
                print ('\rRemote end closed connection;exiting.')
                break
            sys.stdout.write('\rReceived: '+data)
            sys.stdout.flush()
        else:
            print '\rProblem occurred;exiting'
            sys.exit(0)

    spin()

最后的结果非常有趣。。


使用poll()或select()实现事件通知
非阻塞式,可以在不使用线程或进程的情况下同时处理多个网络任务。通过轮询的方式。