Python 网络编程 - socket

如果希望我们的程序能够通过网络进行调用,就需用到网络编程的模块,Python 最基础的网络程序就是 通过 socket 来编写,高级一点的就是所谓的 Web 编程了,底层无非也就是 socket 通信,本文主要介绍 socket 这个模块的用法

socket 基础用法

threading 一样,Python 中 socket 是对 _socket 的封装,_socket 更为底层,我们一般使用 socket

为了方便,我们导入 socket 模块中的所有类

from socket import *

socket 接受几个参数

  • family : 传输使用的地址族,常用的是 AF_INETAF_INET6AF_UNIX
  • type : 传输使用的协议,常用的是 SOCK_STREAM/TCPSOCK_DGRAM/UDP
  • proto: 通常为 0,如果 family 为 AF_CAN 时,应该为 CAN_RAWCAN_BCM
  • fileno: 通常为 None,如果指定了,则会返回一个具有指定文件描述符的套接字返回 # 其实我也不知道什么意思,但是并不妨碍我们写代码
"""
构造函数接受的参数如下
def __init__(self, family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)
"""

sock = socket()
sock.bind(('', 5000)) # bind 方法接受一个元祖,包含监听地址和端口
sock.listen(5) #监听指定地址,5 代表排队连接的最大数量,一般最大为5
conn, addr = sock.accept() #接受连接,如果没有连接会一直阻塞,建立连接后会返回 已经建立连接的 socket 对象和客户端地址
print(conn, addr)

然后程序会阻塞,我们通过 nc 命令建立连接

$ nc localhost 5000

中断并输出

# output
<socket.socket fd=11, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 5000), raddr=('127.0.0.1', 50686)> ('127.0.0.1', 50686)

一个简单的服务端例子

这个简单的例子,会将客户端发送的信息原样返回,能够大致的了解 socket 服务端是怎么做的

from socket import *


class EchoSocketServer(object):
def __init__(self, addr, port, family=AF_INET,
type_=SOCK_STREAM, backlog=0, init=True):

self.addr = addr
self.port = port
self.family = family
self.type_ = type_
self.backlog = backlog
if init is True:
self.init_socket()
else:
self.sock = socket()

def init_socket(self):
"""
init socket object
"""
self.sock = socket(self.family, self.type_)
self.sock.bind((self.addr, self.port))

def _echo(self, conn):
while True:
try:
data = conn.recv(16)
print('Recv', data)
if data:
conn.sendall(data)
print('Send', data)
else:
break
except BrokenPipeError:
break

def run(self):
"""
run echo server
"""
self.sock.listen(self.backlog)
while True:
conn, addr = self.sock.accept()
print('Connect by {} Port {}'.format(*addr))
self._echo(conn)

测试,看似没有什么问题,但是只能接受一个客户端的请求,可以使用多线/进程解决,这个后面再说

>>> serv = EchoSocketServer(addr='', port=5000)	# addr 传空字符串代表监听所有地址

"""
$ nc localhost 5000 #还是通过 nc 命令连接 , !!! 先执行 serv.run()
2
2
3
3
4
4
"""

>>> serv.run()
Connect by 127.0.0.1 Port 51169
Recv b'2\n'
Send b'2\n'
Recv b'3\n'
Send b'3\n'
Recv b'4\n'
Send b'4\n'

一个简单的客户端例子

客户端无非还是那几个方法,sendrecv ,只是要先通过 connect 方法连接服务端

from random import randint
from time import sleep


class EchoSocketClient(object):
def __init__(self, addr, port, family=AF_INET, type_=SOCK_STREAM):
self.addr = addr
self.port = port
self.sock = socket(family, type_)
self.sock.connect((addr, port))

def run(self):
while True:
"""
run echo client
"""
message = str(randint(1, 100))
self.sock.sendall(message.encode('ascii'))
print('Send', message)
data = self.sock.recv(16)
print('Recv', data.decode('utf8'))
sleep(1)

测试

>>> client = EchoSocketClient(addr='localhost', port=5000)

>>> client.run()
Send 58
Recv 58
Send 12
Recv 12
Send 1

实现并发

上面的 EchoSocketServer 同一时刻只能接受一个连接,这不科学,我们可以通过 Thread 来实现并发

from threading import Thread


class EchoThreadSocketServer(EchoSocketServer): #继承 EchoSocketServer 重写 run 方法
def run(self):
self.sock.listen(self.backlog)
while True:
conn, addr = self.sock.accept()
Thread(target=self._echo, args=(conn,)).start()

那就再写个多线程的客户端进行测试吧

from threading import Thread
import logging

logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s [%(threadName)s] %(message)s')


class EchoThreadSocketClient(object):
def __init__(self, addr, port, family=AF_INET, type_=SOCK_STREAM):
self.addr = addr
self.port = port
self.family = family
self.type_ = type_

def _connect(self):
sock = socket(self.family, self.type_)
sock.connect((self.addr, self.port))
while True:
message = str(randint(1, 100))
sock.send(message.encode('ascii'))
logging.info('Send {}'.format(message))
data = sock.recv(16)
logging.info('Recv {}'.format(data.decode('utf8')))
sleep(1)

def run(self):
ts = [Thread(target=self._connect) for _ in range(2)]
[t.start() for t in ts]
[t.join() for t in ts]

使用单进程 EchoServer ,多进程 EchoClient

>>> serv = EchoSocketServer(addr='', port=5000)

>>> serv.run()
Recv b'9'
Send b'9'
Recv b'18'
Send b'18'
Recv b'13'
Send b'13'
Recv b'76'

# 客户端
>>> client = EchoThreadSocketClient(addr='localhost', port=5000)

>>> client.run() #可以看到只有一个线程又返回,其他的都阻塞了
2017-03-15 23:11:53,088 INFO [Thread-30] Send 9
2017-03-15 23:11:53,088 INFO [Thread-30] Recv 9
2017-03-15 23:11:53,089 INFO [Thread-29] Send 95
2017-03-15 23:11:54,093 INFO [Thread-30] Send 18
2017-03-15 23:11:54,093 INFO [Thread-30] Recv 18
2017-03-15 23:11:55,099 INFO [Thread-30] Send 13
2017-03-15 23:11:55,099 INFO [Thread-30] Recv 13

使用多进程 EchoServer, 多进程 EchoClient

结果看起来很科学

>>> serv = EchoThreadSocketServer(addr='', port=5000)

>>> serv.run()
Recv b'4'
Send b'4'
Recv b'40'
Send b'40'
Recv b'48'
Recv b'38'
Send b'38'

>>> client = EchoThreadSocketClient(addr='localhost', port=5000)

>>> client.run() #每一个线程都有了返回
2017-03-15 23:15:20,410 INFO [Thread-88] Send 4
2017-03-15 23:15:20,410 INFO [Thread-89] Send 40
2017-03-15 23:15:20,411 INFO [Thread-88] Recv 4
2017-03-15 23:15:20,411 INFO [Thread-89] Recv 40
2017-03-15 23:15:21,413 INFO [Thread-88] Send 48
2017-03-15 23:15:21,413 INFO [Thread-88] Recv 48
2017-03-15 23:15:21,414 INFO [Thread-89] Send 38

socketserver

Python 标准库中有一个叫 socketserver # python 2 为 SocketServer 的模块能让我们更简单的编写网络程序

更复杂的就不介绍了,请自行查阅 socketserver 官方文档,只介绍一点基础的使用,其实我也不会用,哈哈

fibserver

这次不写 echoserver 了,写计算 菲波那切数列的服务端程序,不要吐槽这个算法,这里使用递归来计算是为了降低响应效率,否则算的太快了。。。

import socketserver


def fib(n):
if n < 2:
return 1
return fib(n - 1) + fib(n - 2)


class MyFibHandler(socketserver.BaseRequestHandler): #从BaseRequestHandler继承
def handle(self): #重写 handle
while True:
data = int(self.request.recv(16))
if not data:
return
message = (str(fib(data)) + '\n').encode('ascii')
self.request.sendall(message)

测试

>>> server = socketserver.TCPServer(('localhost', 5000), MyFibHandler)

>>> server.serve_forever()


#客户端

"""
$ nc localhost 5000 #看起来运行的很不错
2
2
8
34
9
55
17
2584
"""

#如果需要多线程实现并发,只需要这样启动就行了,更多的还是看文档吧

>>> server = socketserver.ThreadingTCPServer(('localhost', 5000), MyFibHandler)

关于 socketserver,我真的不知道更多东西了

参考资料:

py how to socket

David Beazley: Python Concurrency From the Ground Up: LIVE! 这个强烈推荐

Python Doc socketserver