Python 网络编程攻略 第一章

1.1 简介

因为要放上具体的代码,今天就从新浪转战CSDN了

原来对Python有了一定的了解,2和3的差距有时候还是挺折腾人的。原来刷过几个oj,上面都是用Python解决的,对于大部分人来说问题应该不是特别大。

今天开始lz就按照《Python Network Programming Cookbook》这本书来进行追踪,求监督!


ps.第一章还是按照Python3进行代码的,后面的章节,因为有涉及到操作系统的问题,话说我一直不知道为什么Python for Win里面竟然会有os.fork()……后面就直接在Ubuntu 12.04上面实验了。Ubuntu自带Python 2.7,正好和书本当中的版本一样。


1.2 打印设备名和IPv4地址

<span style="font-size:18px;"><span style="font-size:18px;"><span style="font-size:18px;"># 1_1_local_machine_info.py

import socket

def print_machine_info():
    host_name = socket.gethostname()
    ip_address = socket.gethostbyname(host_name)
    print ("host name is %s " %host_name)
    print ("ip address is %s" %ip_address)

if __name__ == '__main__':
    print_machine_info()</span></span></span>
__name__注意是两个下划线,表示调用程序的进程名,如果在命令行中运行脚本,__name__的值就是'__main__'

print函数就是前面格式化,用引号包住,后面一个%,然后再把需要的输出用括号括起来。

1.3 获取远程设备的IP地址

<span style="font-size:18px;"><span style="font-size:18px;"><span style="font-size:18px;"># 1_2_remote_machine_info.py

import socket

def get_remote_machine_info():
    remote_host = 'www.python.org'
    try:
        print("IP address: %s" %socket.gethostbyname(remote_host))
    except socket.error as err_msg:
        print("%s %s" %(remote_host, err_msg))

if __name__ == '__main__':
    get_remote_machine_info()</span></span></span>

主机名改变即可,socket.gethostbyname的参数改变。


1.4 将IPv4地址转换成不同的格式

<span style="font-size:18px;"><span style="font-size:18px;"><span style="font-size:18px;"># 1_3_ipv4_address_conversion.py

import socket

from binascii import hexlify

def convert_ipv4_address():
    for ip_addr in ['127.0.0.1','192.168.0.1']:
        packed_ip_addr = socket.inet_aton(ip_addr)
        unpacked_ip_addr = socket.inet_ntoa(packed_ip_addr)
        print(" IP address %s  =>  Packed: %s , UnPacked: %s "
              %(ip_addr, hexlify(packed_ip_addr), unpacked_ip_addr))
              
if __name__ == '__main__':
    convert_ipv4_address()</span></span></span>

调用了binascii模块的hexlify函数,以十六进制形式表示二进制数据。


1.5 通过制定的端口和协议找到服务名

<span style="font-size:18px;"><span style="font-size:18px;"><span style="font-size:18px;"># 1_4_finding_service_name.py

import socket

def find_service_name():
    protocolname = 'tcp'
    for port in [80,25]:
    #for port in range(20,81):
        print("Port: %s  =>  service name: %s  " %(port, socket.getservbyport(port,protocolname)))
    print("Port: %s  =>  service name: %s " %(53, socket.getservbyport(53, 'udp')))    

if __name__ == '__main__':
    find_service_name()</span></span></span>


1.6 主机字节序和网络字节序之间相互转换

<span style="font-size:18px;"><span style="font-size:18px;"><span style="font-size:18px;"># 1_5_integer_conversion.py

import socket

def convert_integer():
    data = 1234
    # 32 bit
    print("original: %s => Long host byte order: %s, Network byte order: %s"
        % (data, socket.ntohl(data), socket.htonl(data)))
    # 16 bit
    print("original: %s => Short host byte order: %s, Network byte order: %s"
        % (data, socket.ntohs(data), socket.htons(data)))
    data1 = socket.htonl(data)
    data2 = socket.ntohl(data1)
    print("%s %s %s" %(data,data1,data2))

if __name__ == '__main__':
    convert_integer()</span></span></span>

socket库中的类函数ntohl()把网络字节序转换成了长整形主机字节序。函数名中的n表示网络,h表示主机,l表示长整形,s表示短整型,即16位。

1.7 设定并获取默认的套接字超时时间

<span style="font-size:18px;"><span style="font-size:18px;"><span style="font-size:18px;"># 1_6_socket_timeout.py

import socket

def test_socket_timeout():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    print("Default socket timeout: %s" %s.gettimeout())
    s.settimeout(100)
    print("Current socket timeout: %s" %s.gettimeout())

if __name__ == '__main__':
    test_socket_timeout()
</span></span></span>
需要创建套接字对象。套接字构造方法第一个参数是地址族,第二个参数是套接字类型。

socket=socket.socket(family,type),family的值可以是AF_UNIX(Unix域,用于同一台机器上的进程通讯),也可以是AF_INET(对于IPv4协议的TCP和UDP)。至于type参数,SOCK_STREAM(流套接字)或者SOCK_DGRAM(数据报文套接字),SOCK_RAW(raw套接字)。

gettimeout()方法获取套接字的超时时间,再调用settimeout()方法修改超时时间。传给settimeout()的参数可以是秒数,非负浮点数,也可以是None。如果是None,就禁用了套接字操作的超时检测。

这里提到了socket,那么就说一下socket套接字的用法。

先说一下SNTP服务器,(这样1.14好像没有写的必要了?)这里感谢点击打开链接,写的很详细,大家可以参考。后面还有更加复杂的多线程,后面章节会说明如何编写。

server端:

第一步,创建一个socket对象,参数已经说了,见上。

第二步,socket进行绑定到一个地址上面,进行监听。

<span style="font-size:18px;"><span style="font-size:18px;"><span style="font-size:18px;">socket.bind(address)
sock.listen(backlog)</span></span></span>

address是一个(ip,port)的双元素二元组,绑定的时候需要这么操作 socket.bind((ip,port))

backlog表示最多连接数,server收到连接请求之后,这些请求会进行排队,如果队列已经满了,超过backlog数目,那么拒绝请求。

第三步,server通过socket.accept()接收client端来的请求链接。

代码如下,一般会有一个while True的循环,表示服务器一直进行监听。

<span style="font-size:18px;"><span style="font-size:18px;"><span style="font-size:18px;">while True:
	client_socket, address = socket.accept()
	data = client_socket.recv(data_payload).decode()
	client_socket.sendall(msg.encode())</span></span></span>
调用accept方法的时候,socket会进入waiting(或阻塞)状态。client端请求链接,建立连接并返回服务器。accept()方法返回一个含有两个元素的双元组,形如client_socket,address。第一个client_socket是新的socket对象,server通过它与client发送信息,address是client的address。

第四步,接收、处理和发送信息。

代码和第三步的代码在一起了。这里需要注意一点,Python3.4有一个捉鸡的问题,上文档

In Python3, bytes strings and unicode strings are now two different types. Since sockets are not aware of string encodings, they are using raw byte strings, that have a slightly different interface from unicode strings.

encode it when unicode -> byte string

decode it when byte -> unicode strng

When you use client_socket.send(data), replace it by client_socket.send(data.encode())

When you get data, using data = client_socket.recv(1024), replace it by data = client_socket.recv(1024).decode()

就是说,平时代码当中的都是byte string,但是当我在socket当中进行传输的时候,用的是unicode string,这两个类型的string在Python2当中是不区分的,而在Python3就区分了。所以需要手动的encode()和decode()一下。

client端就留到1.14再详细描述。大家先感受一下server端,其实和java的很像。


1.8 优雅地处理套接字错误

<span style="font-size:18px;"><span style="font-size:18px;"><span style="font-size:18px;"># 1_7_socket_errors.py

import socket
import sys
import argparse

def main():
    # setup argument parsing
    parser = argparse.ArgumentParser(description = 'Socket Error Examples')
    parser.add_argument("--host", action="store", dest="host", required=False)
    parser.add_argument("--port", action="store", dest="port", required=False)
    parser.add_argument("--file", action="store", dest="file", required=False)
    given_args = parser.parse_args()
    host = given_args.host
    port = int(given_args.port)
    filename = given_args.file
    print("host: %s   port: %s   filename: %s" %(host, port, filename))

    # First try-except block -- create socket
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    except socket.error as e:
        print("Error creating socket: %s" %e)
        sys.exit(1)

    # Second try-except block -- connect to given host/port
    try:
        s.connect((host,port))
    except socket.gaierror as e:
        print("Address-related error connecting to server %s" %e)
        sys.exit(1)
    except socket.error as e:
        print("Connection error %s" %e)
        sys.exit(1)

    # Third try-except block -- sending data
    try:
        s.sendall(('GET %s HTTP/1.0\r\n\r\n' %filename).encode())
        #s.sendall('GET 1_7_socket_errors.py HTTP/1.0\r\n\r\n'.encode())
    except socket.error as e:
        print("Error sending data: %s" %e)
        sys.exit(1)

    # Fourth try-except block -- waiting to receive data from remote host
    while 1:
        try:
            buf = s.recv(2048).decode()
        except socket.error as e:
            print("Error receiving data: %s" %e)
            sys.exit(1)
        if not len(buf):
            break
        # write the received data
        sys.stdout.write(buf)

if __name__ == '__main__':
    main()
</span></span></span>
注意一下语法

Python3: except socket.error as e

Python2: except socket.error, e

这个程序要运行,IDLE模式可能不行,还得在Linux的shell模式,或者windows的cmd模式下运行。输入

<span style="font-size:18px;"><span style="font-size:18px;"><span style="font-size:18px;">python 1_7_socket_errors.py --host=www.python.org --port=80</span></span></span>

可以看到这里有一个args参数的用法,和java的public static void main(String[] args)   args[0],args[1]功能一样。

<span style="font-size:18px;"><span style="font-size:18px;"><span style="font-size:18px;">parser = argparse.ArgumentParser(description="")
parser.add_argument( ……, dest='dest1', …… )
given_args = parser.parse_args()
variable1 = given_args.dest1</span></span></span>

ArgumentParser Object

Create a new ArgumentParser object. All parameters should be passed as keyword arguments. Each parameter has its own more detailed description below, but in short they are:

  • prog - The name of the program (default: sys.argv[0])
  • usage - The string describing the program usage (default: generated from arguments added to parser)
  • description - Text to display before the argument help (default: none)
  • epilog - Text to display after the argument help (default: none)
  • parents - A list of ArgumentParser objects whose arguments should also be included
  • formatter_class - A class for customizing the help output
  • prefix_chars - The set of characters that prefix optional arguments (default: ‘-‘)
  • fromfile_prefix_chars - The set of characters that prefix files from which additional arguments should be read (default: None)
  • argument_default - The global default value for arguments (default: None)
  • conflict_handler - The strategy for resolving conflicting optionals (usually unnecessary)
  • add_help - Add a -h/–help option to the parser (default: True)

add_argument() method

ArgumentParser.add_argument(name or flags...[action][nargs][const][default][type][choices][required][help][metavar][,dest]) Define how a single command-line argument should be parsed. Each parameter has its own more detailed description below, but in short they are:

  • name or flags - Either a name or a list of option strings, e.g. foo or -f, --foo.
  • action - The basic type of action to be taken when this argument is encountered at the command line.
  • nargs - The number of command-line arguments that should be consumed.
  • const - A constant value required by some action and nargs selections.
  • default - The value produced if the argument is absent from the command line.
  • type - The type to which the command-line argument should be converted.
  • choices - A container of the allowable values for the argument.
  • required - Whether or not the command-line option may be omitted (optionals only).
  • help - A brief description of what the argument does.
  • metavar - A name for the argument in usage messages.
  • dest - The name of the attribute to be added to the object returned by parse_args().

ArgumentParse的参数,用的比较多的是description,表示的是在help之后显示的内容

add_argument的参数,用的是我们在代码中显示的几个

name of flags: 在shell或者cmd模式下输入的名字

action: 这里用的都是store,表示的是保存某个参数的数值

dest: 用来指代输入的变量

type: 数值类型

required: 是否必须输入


1.9 修改套接字发送和接收的缓冲区大小

# 1_8_modify_buff_size.py

import socket

SEND_BUF_SIZE = 4096
RECV_BUF_SIZE = 4096

def modify_buff_size():
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    # Get the size of the socket's send buffer
    bufsize = sock.getsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF)
    print("Buffer size [Before]: %d " %bufsize)

    sock.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1)
    sock.setsockopt(
        socket.SOL_SOCKET,
        socket.SO_SNDBUF,
        SEND_BUF_SIZE)
    sock.setsockopt(
        socket.SOL_SOCKET,
        socket.SO_RCVBUF,
        RECV_BUF_SIZE)

    bufsize = sock.getsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF)
    print("Buffer size [After]: %d" %bufsize)

if __name__ == '__main__':
    modify_buff_size()
setsockopt()方法接受三个参数:level,optname和value。其中,optname是选项名,value是该选项的值。第一个参数所用的符号常量可以在socket模块中查看。level定义了哪个选项将被使用,通常情况下是SOL_SOCKET,意思是正在使用的socket选项。


1.10 把套接字改成阻塞式或给阻塞模式

# 1_9_socket_modes.py

import socket

def test_socket_modes():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.setblocking(1)
    s.settimeout(0.5)
    s.bind(("127.0.0.1",0))

    socket_address = s.getsockname()
    print("Trivial Server launched on socket: %s" %str(socket_address))
    while(1):
        s.listen(1)

if __name__ == '__main__':
    test_socket_modes()

setblocking(1)把套接字设置为阻塞模式,setblocking(0)把套接字设置为非阻塞模式。


1.11 重用套接字地址

# 1_10_reuse_socket_address.py

import socket
import sys

def reuse_socket_addr():
    sock = socket.socket( socket.AF_INET, socket.SOCK_STREAM )

    # Get the old state of the SO_REUSEADDR option
    old_state = sock.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR)
    print("Old sock state: %s" %old_state)

    # Enable the SO_REUSEADDR option
    sock.setsockopt( socket.SOL_SOCKET, socket.SO_REUSEADDR, 1 )
    new_state = sock.getsockopt( socket.SOL_SOCKET, socket.SO_REUSEADDR )
    print("New sock state: %s" %new_state)

    local_port = 8282

    srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    srv.bind(('',local_port))
    srv.listen(1)
    print("Listening on port: %s" %local_port)
    while True:
        try:
            connection,addr = srv.accept()
            print("Connected by %s: %s" %(addr[0], addr[1]))
        except KeyboardInterrupt:
            break
        except socket.error as msg:
            print("%s" %msg)
    
    

if __name__ == '__main__':
    reuse_socket_addr()

有时候需要在同一个端口上运行套接字服务器,比如client需要一直连接指定服务器端口,这么操作就很有用,因为无需改变服务器端口。

在一个cmd下输入telnet localhost 8282,即可连接到这个服务器。

代码中SO_REUSEADDR表示,当socket关闭后,本地端用于改socket的port立即可以被重用。默认情况下,只有经过系统定义的一段时间后才能被重用。


1.12 从网络时间服务器获取并打印当前时间

# 1_11_print_machine_time.py

import ntplib
from time import ctime

def print_time():
    ntp_client = ntplib.NTPClient()
    response = ntp_client.request('pool.ntp.org')
    print ctime(response.tx_time)

if __name__ == '__main__':
    print_time()

用了一个ntplib,通过Network Time Protocol处理客户端和服务器之间的通信。


1.13 编写一个SNTP客户端

# 1_12_stnp_client.py

import socket
import struct
import sys
import time

NTP_SERVER = '0.uk.pool.ntp.org'
TIME1970 = 2208988800

def sntp_client():
    client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    data = '\x1b' + 47 * '\0'
    client.sendto(data.encode(), (NTP_SERVER, 123))
    data,address = client.recvfrom(1024)
    if data:
        print('Response received from:', address)
    t = struct.unpack('!12I', data)[10]
    t -= TIME1970
    print('\ttime=%s' %time.ctime(t))

if __name__ == '__main__':
    sntp_client()

这个SNTP客户端创建了一个套接字链接,然后通过协议发送数据。从NTP服务器受到数据后,用struct模块取出数据。最后,减去1970年1月1日对应的时间戳,再用Python内置的time提供的ctime()方法打印时间。


1.14 编写一个简单的回显客户端/服务器应用

server端

# 1_13a_echo_server.py

import socket
import sys
import argparse

host = 'localhost'
data_payload = 2048
backlog = 5

def echo_server(port):
    """A simple echo server"""
    # Create a TCP socket
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # Enable reuse address/port
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    # Bind the socket to the port
    server_address = (host,port)
    print("Starting up echo server on %s port %s" %server_address)
    sock.bind(server_address)
    # Listening to clients, backlog argument specifies the max no. of queued connections
    sock.listen(backlog)
    while True:
        print("Waiting to receive message from client")
        client, address = sock.accept()
        print("The client address is %s" %(str(address)))
        data = client.recv(data_payload).decode()
        if data:
            print("Data: %s" %data)
            client.send(data.encode())
            print("sent %s bytes back to %s" %(data, address))
        print()
        #end connection
        client.close()

if __name__ == '__main__':
    #parser = argparse.ArgumentParser(description='Socket Server Example')
    #parser.add_argument('--port',action="store", dest='port', type=int, required=True)
    #given_args = parser.parse_args()
    #port = given_args.port
    #echo_server(port)
    echo_server(9900)

运行 python 1_13a_echo_server.py --port=9900


client端

# 1_13b_echo_client

import socket
import sys
import argparse

host = 'localhost'

def echo_client(port):
    """A simple echo client"""
    # Create a TCP/IP socket
    sock = socket.socket( socket.AF_INET, socket.SOCK_STREAM )
    # Connect the socket to the server
    server_address = (host,port)
    print("Connecting to %s port %s" %server_address)
    sock.connect(server_address)

    # Send data
    try:
        # Send data
        message = "Test message. This will be echoed"
        print("Sending %s" %message)
        sock.sendall(message.encode())
        # Look for response
        amount_received = 0
        amount_expected = len(message)
        while amount_received < amount_expected:
            data = sock.recv(16).decode()
            amount_received += len(data)
            print("Received: %s" %data)
    except socket.error as e:
        print('Socket error: %s' %str(e))
    except Exception as e:
        print('Other exception: %s' %str(e))
    finally:
        print('Closing connection to the server')
        sock.close()

if __name__ == '__main__':
    #parser = argparse.ArgumentParser(description = 'Socket Server Example')
    #parser.add_argument('--port', action='store', dest='port', type=int, required=True)
    #given_args = parser.parse_args()
    #port = given_args.port
    #echo_client(port)
    echo_client(9900)

运行 python 1_13b_echo_client.py --port=9900


这里接着1.7继续讲一下客户端的socket创建,如果理解了server端,client其实就简单很多了。代码如下

sock = socket.socket(family,type)
sock.connect((host,port))
sock.recv(1024).decode()
sock.sendall(msg.encode())
sock.close()

第一步,创建一个socket。

第二步,使用sock的connect方法,连接服务器。

第三步,接受、处理、发送信息。

第四步,关闭socket。


妈蛋,不小心排版错了,重新来了一遍,差点弄死我,眼睛废了的感觉。

anyway,总算是结束了第一章。下面继续哈。


我去,这个格式我调整了三次了,每次都没问题,发表之后都是问题……懒得弄了……能看懂就行。


版权声明:本文为miss_snow_m原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。