Python网络编程

什么是socket

Socket(也称作套接字)是一组接口,是应用层与 TCP/IP协议族 通信的中间软件抽象层,它对TCP/IP协议进行了实现,应用层需要网络通信,直接调用这些接口即可~
 
从应用层的角度,也可以简单地将 Socket 理解为 ip+port,ip用来定位互联网中的一台主机,port用来定位该主机上的应用程序,所以通过 ip+port 能够找到需要通信的另一个程序,通信过程的底层由 Socket 模块实现~

基于文件类型的套接字

套接字家族名称:AF_UNIX

基于文件的套接字就是通过对同一个文件的读写来完成进程间的通信。若两个进程运行在同一台服务器上,使用这种方式来通信效率更高~

基于网络类型的套接字

套接字家族名称:AF_INET

跨越网络的通信使用 AF_INET,还有AF_INET6,用于ipv6。常用的就这几个,剩下的无需关心~

基于TCP连接的socket

创建TCP连接时,由客户端主动发起连接,建立连接之后,基于这个连接开始通信。TCP连接是可靠的连接~
  
服务端

import socket

sk = socket.socket()
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 
sk.bind(('127.0.0.1', 8888))   # 绑定套接字
sk.listen(10)               # 监听连接,10表示 server 端最多同时响应10个连接
conn, _ = sk.accept()       # 接收连接
msg = conn.recv(1024)       # 接收 client 端发来的信息
print(msg)

conn.send(b'hi')      # 向客户端发送信息
conn.close()        # 关闭连接
sk.close()          # 关闭 server端套接字 

说明:
setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 表示对IP地址和端口进行重用,可避免端口已被占用的问题(上一次服务端程序退出之后,其使用的端口还未完全关闭)

OSError: [Errno 48] Address already in use

 
这里的 socket.socket() 省略了默认参数,等同于 socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
family 指定 套接字家族,可以是AF_UNIX或者AF_INET
type 指定套接字类型,是面向连接的(SOCK_STREAM)还是非连接(SOCK_DGRAM),基于TCP的连接使用 SOCK_STREAM,基于UDP的使用 SOCK_DGRAM~

客户端

import socket

sk = socket.socket()
ip_addr = ('127.0.0.1', 8888)
sk.connect(ip_addr)

sk.send(b'hello')
rece_msg = sk.recv(1024)
print(rece_msg)
sk.close()

基于UDP的socket

TCP是基于可靠的连接,并且通信双方都以流的形式发送数据,相对于TCP,UDP则是面向无连接的协议。
使用UDP协议通信,不需要建立连接,只需要知道对方的IP地址和端口号,就可以直接发送数据包。使用UDP传输数据是不可靠的,发送端只管发送,并不会对数据包的到达进行确认,但相对于TCP,它的优点是速度快~
  
服务端

import socket

udp_sk = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp_sk.bind(('127.0.0.1', 8888))
msg, addr = udp_sk.recvfrom(1024)    # 接受数据
print(msg)
udp_sk.sendto(u'你好'.encode('utf-8'), addr)   # 发送数据
udp_sk.close()

创建 socket 时指定套接字类型 为 socket.SOCK_DGRAM。绑定端口之后(bind),不需要 listen,便可以直接 recvfrom 接受客户端的数据

 
客户端

import socket

udp_sk = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
ip_addr = ('127.0.0.1', 8888)
udp_sk.sendto(b'from udp client', ip_addr)

msg, addr = udp_sk.recvfrom(1024)
print(msg.decode('utf-8'))
udp_sk.close()

客户端创建基于UDP的Socket后,不需要连接(connect),可直接使用sendto发送数据

socket的其他方法介绍

服务端套接字函数
s.bind()    绑定(主机,端口号)到套接字,在AF_INET下,以元组(host,port)的形式表示地址。
s.listen(n)  开始TCP监听,你表示能同时建立的连接数据
s.accept()  被动接受TCP客户的连接,(阻塞式)等待连接的到来

客户端套接字函数
s.connect()     主动初始化TCP服务器连接,一般address的格式为元组(hostname,port),如果连接出错,返回socket.error错误。
s.connect_ex()  connect()函数的扩展版本,出错时返回出错码,而不是抛出异常

公共用途的套接字函数
s.recv(bufsize)      接收TCP数据,数据以bytes形式返回,bufsize指定要接收的最大数据量。
s.send(string)       发送TCP数据,将string(bytes)中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于string的字节大小。
s.sendall(string)     完整发送TCP数据。将string中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常。
s.recvfrom(buffer)    接收UDP数据,与recv()类似,但返回值是(data,address)。其中data是包含接收数据的字符串,address是发送数据的套接字地址。
s.sendto(string, addr)      发送UDP数据,将数据发送到套接字,addr是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。
s.getpeername()     连接到当前套接字的远端的地址
s.getsockname()     当前套接字的地址
s.getsockopt()      返回指定套接字的参数
s.setsockopt()      设置指定套接字的参数
s.close()           关闭套接字

面向锁的套接字方法
s.setblocking()     设置套接字的阻塞与非阻塞模式
s.settimeout()      设置阻塞套接字操作的超时时间
s.gettimeout()      得到阻塞套接字操作的超时时间

面向文件的套接字的函数
s.fileno()          返回套接字的文件描述符
s.makefile()        创建一个与该套接字相关的文件

简单说明一下 send 和 sendall 方法的区别:
上面已经说明 send(string) 方法发送的数据量可能小于string的字节大小,就是说可能无法发送string中的所有数据,所以简单起见,一般使用sendall方法。如下两种方式是等价的:

# sendall
sock.sendall('Hello world\n')

# send
buffer = 'Hello world\n'
while buffer:
    bytes = sock.send(buffer)
    buffer = buffer[bytes:]

黏包现象

如下示例是远程执行命令的程序,客户端发送命令到服务器端,服务器端执行完成后,将结果返回给客户端~
 
服务端

ip_port = ('127.0.0.1', 8888)
BUFSIZE = 1024

tcp_sk_server = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
tcp_sk_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
tcp_sk_server.bind(ip_port)
tcp_sk_server.listen(5)

while True:
    conn, addr = tcp_sk_server.accept()
    print("from %s:%s" % (addr[0], addr[1]))

    while True:
        cmd = conn.recv(BUFSIZE)    # recv 方法会阻塞,直到接收到数据;若是连接关闭,则会接收一个 b'',程序继续往后执行
        if len(cmd) == 0: break

        res = subprocess.Popen(cmd, shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
        stderr = res.stderr.read()
        stdout = res.stdout.read()

        if stderr:
            send_mes = stderr
        else:
            send_mes = stdout

        conn.sendall(send_mes)

客户端

import socket

ip_port = ('127.0.0.1', 8888)
BUFSIZE = 100

tcp_sk_client = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
tcp_sk_client.connect_ex(ip_port)

while True:
    cmd = input(">>: ").strip()
    if len(cmd) == 0: continue
    if cmd == 'quit': break

    tcp_sk_client.send(cmd.encode('utf-8'))
    cmd_res = tcp_sk_client.recv(BUFSIZE)
    print(cmd_res.decode('utf-8'))

tcp_sk_client.close()

注意客户端接收数据的 BUFSIZE 调整成了100,如下是客户端执行的返回结果:

>>: ls /tmp
VMwareDnD
com.apple.launchd.Q7QRr2IZct
com.apple.launchd.gOYBDHgesI
powerlog

>>: ifconfig
lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> mtu 16384
    options=1203<RXCSUM,TXCSUM,TXSTATUS,SW_TIM
>>: ls /tmp
ESTAMP>
    inet 127.0.0.1 netmask 0xff000000 
    inet6 ::1 prefixlen 128 
    inet6 fe80::1%lo0 prefixlen 6
>>: 

第一个命令 'ls /tmp' 完整的输出了,但是第二个命令由于其返回数据的长度大于100,只显示了一部分,剩下没有显示的在下一个返回结果中输出,而下一个命令的返回结果应该还在缓存中,这就是黏包现象

发生黏包现象的原因

TCP协议是基于数据流的,数据发送之后,数据的长度对于客户端的应用程序而言是不可见的,客户端程序在从缓冲区提取数据的时候不知道一段数据从哪里开始到哪里结束,这就造成了黏包现象。
还有就是TCP协议中的Nagle算法,将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包,发送。这也是造成客户端接收数据时产生黏包现象的原因~
 
总之面向流的通信(基于TCP协议的通信)是无消息保护边界的,这是造成黏包的主要原应~
 
UDP协议是无连接的,面向消息的。由于UDP支持的是一对多模式,接收端的套接字缓冲区采用了链式结构来记录每一个到达的UDP包,在每个UDP包中有消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
所以基于UDP协议的通信是不会有黏包现象的。客户端使用 recvfrom(bufsize) 接收bufsize 大小的数据后,这个消息剩下的数据就丢失了。下一次接收数据从另一个消息的开头开始提取。而基于TCP协议的通信,客户端使用 recv(bufsize) 接收 bufsize 大小的数据后,剩下的数据会依旧留在缓存中,下一次接收数据会从上一次结束的地方开始提取~
 
综上所述,基于TCP协议的通信,以下两种情况下会出现黏包:
 
情况_1
发送端将多次间隔较小且数据量小的数据,合并成一个大的数据块发送出去,造成接收端的黏包~
 
服务端(接收端)

import socket

ip_port = ('127.0.0.1', 8888)
BUFSIZE = 1024

sk = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sk.bind(ip_port)
sk.listen(5)

conn, addr = sk.accept()
info_1 = conn.recv(BUFSIZE)
info_2 = conn.recv(BUFSIZE)
print('info_1: %s' % info_1.decode('utf-8'))
print('info_2: %s' % info_2.decode('utf-8'))
conn.close()

客户端(发送端)

import socket

ip_port = ('127.0.0.1', 8888)

sk = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
sk.connect(ip_port)

sk.send(b'hello')
sk.send(b'hi')
sk.close()

在服务端(接收端)的输出结果:

info_1: hellohi
info_2: 

情况_2
接收端在接收数据时只接收了一部分,下一次接收数据时,会接受到上一次遗留的数据~
 
服务端(接收端)

import socket

ip_port = ('127.0.0.1', 8888)
# BUFSIZE = 1024

sk = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sk.bind(ip_port)
sk.listen(5)

conn, addr = sk.accept()
info_1 = conn.recv(2)      # 这里将 BUFSIZE 仅设置成2字节
info_2 = conn.recv(5)
print('info_1: %s' % info_1.decode('utf-8'))
print('info_2: %s' % info_2.decode('utf-8'))
conn.close()

客户端(发送端)

import socket

ip_port = ('127.0.0.1', 8888)

sk = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
sk.connect(ip_port)

sk.send(b'hello')
sk.send(b'hi')
sk.close()

在服务端(接收端)的输出结果:

info_1: he
info_2: llohi

第一次发送的数据出现在下一次的接收中,上述给出的远程执行命令的程序就属于这种情况~

解决方式1

解决方式,就是在发送数据前提前通知数据的长度,这样在接收数据时仅接收指定长度的数据,从而避免黏包~
 
服务端

import socket
import subprocess

ip_port = ('127.0.0.1', 8888)
BUFSIZE = 1024

tcp_sk_server = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
tcp_sk_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
tcp_sk_server.bind(ip_port)
tcp_sk_server.listen(5)

while True:
    conn, addr = tcp_sk_server.accept()
    print("from %s:%s" % (addr[0], addr[1]))

    while True:
        cmd = conn.recv(BUFSIZE)
        if len(cmd) == 0: break

        res = subprocess.Popen(cmd, shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
        stderr = res.stderr.read() 
        stdout = res.stdout.read() 
        if stderr:
            send_mes = stderr
        else:
            send_mes = stdout

        data_length = len(send_mes)
        conn.send(str(data_length).encode('utf-8'))

        back_data = conn.recv(BUFSIZE).decode('utf-8')
        if back_data == 'OK':
            conn.sendall(send_mes)

conn.close()

客户端

import socket

ip_port = ('127.0.0.1', 8888)
BUFSIZE = 1024

tcp_sk_client = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
tcp_sk_client.connect_ex(ip_port)

while True:
    cmd = input(">>: ").strip()
    if len(cmd) == 0: continue
    if cmd == 'quit': break

    tcp_sk_client.send(cmd.encode('utf-8'))

    # 接收长度信息
    data_length = int(tcp_sk_client.recv(BUFSIZE).decode('utf-8'))
    tcp_sk_client.send('OK'.encode('utf-8'))

    cmd_res = b''
    recv_length = 0
    while recv_length < data_length:
        cmd_res += tcp_sk_client.recv(BUFSIZE)
        recv_length += BUFSIZE

    print(cmd_res.decode('utf-8'))

tcp_sk_client.close()

在这个示例中,虽然服务端提前发送了数据的长度信息,但是这个长度信息的长度,客户端任然不知道,这样若是发送完数据的长度信息之后,直接发送数据,依旧会存在黏包现象;因为 数据的长度信息 的数据量很小,发送端在发送的时候可能会将其和后面的部分数据 合并成一个大的数据块发送出去,接收端不知道 长度信息 的数据长度,两个数据之间也没有边界。
而在这个示例中,发送端在发送完数据的长度之后,接收了客户端(接收端)的反馈信息(tcp_sk_client.send('OK'.encode('utf-8'))),确认是"OK"之后再发送数据信息。这就相当于 两个数据之间 有了 边界(发送端 在发送数据长度 和 发送数据 之间有一个 recv 动作),绕过了黏包问题~
 
这种解决方式存在一个问题,就是发送端和接收端多了一次交互过程,这可能会放大网络延迟带来的性能损耗~
另一种方式就是使用struct模块来解决黏包问题~

struct 模块

struct 模块可以一个类型的数据转成固定长度的 bytes ~

import struct

struct_num = struct.pack('i', 123)
print(struct_num)        # b'{\x00\x00\x00'

num = struct.unpack('i', struct_num)
print(num[0])               # 123

'i' 表示 要转换的类型是 int类型~
Python网络编程
 
关于struct模块的具体使用方式可参见:http://www.cnblogs.com/coser/archive/2011/12/17/2291160.html

解决方式2(使用 struct 模块)

使用struct 模块,将数据的长度转换成一个4字节数字。这样接收端就知道了表示数据长度的数据量的大小,直接接收即可,省去了发送接送反馈信息这一步~
 
服务端

import socket
import subprocess
import struct

ip_port = ('127.0.0.1', 8888)
BUFSIZE = 1024

tcp_sk_server = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
tcp_sk_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
tcp_sk_server.bind(ip_port)
tcp_sk_server.listen(5)

while True:
    conn, addr = tcp_sk_server.accept()
    print("from %s:%s" % (addr[0], addr[1]))

    while True:
        cmd = conn.recv(BUFSIZE)
        if len(cmd) == 0: break

        res = subprocess.Popen(cmd, shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
        stderr = res.stderr.read()
        stdout = res.stdout.read()
        if stderr:
            send_mes = stderr
        else:
            send_mes = stdout

        conn.send(struct.pack('i', len(send_mes)))
        conn.sendall(send_mes)

conn.close()

客户端

import socket
import struct

ip_port = ('127.0.0.1', 8888)
BUFSIZE = 1024

tcp_sk_client = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
tcp_sk_client.connect_ex(ip_port)

while True:
    cmd = input(">>: ").strip()
    if len(cmd) == 0: continue
    if cmd == 'quit': break

    tcp_sk_client.send(cmd.encode('utf-8'))

    # 接收长度信息
    data_length = struct.unpack('i', tcp_sk_client.recv(4))[0]

    cmd_res = b''
    recv_length = 0
    while recv_length < data_length:
        cmd_res += tcp_sk_client.recv(BUFSIZE)
        recv_length += BUFSIZE

    print(cmd_res.decode('utf-8'))

tcp_sk_client.close()

发送端发送完长度信息后,可直接发送数据,因为接收端知道长度信息的数据量(4字节),可直接接收~
 
其实解决黏包问题的关键就是让接收端知道将要接收的数据的长度,从而不多不少的接收数据~

socketserver

SocketServer 对 socket 进行了封装,内部使用 IO多路复用 以及 “多线程” 和 “多进程” ,从而实现并发处理多个客户端请求的 Socke t服务端。

在 SocketServer 中有这些类:ThreadingTCPServer、TCPServer、ForkingTCPServer 是基于TCP实现的。
TCPServer 并不是并发的,在接收到请求后逐一进行处理,如果前一个的 handle 没有结束,那么其他的请求将不会处理。
ThreadingTCPServer 和 ForkingTCPServer 则可以并发处理请求,其实现原理是 没接收到一个请求就开启一个线程或者进程进行处理。ThreadingTCPServer 通过建立新线程来运行handle,ForkingTCPServer 则通过建立新进程来运行 handle ~
 
远程执行命令的程序 通过 socketserver 来实现
 
服务端

import socketserver
import subprocess
import json
import struct

IP, PORT = '127.0.0.1', 9999
BUFSIZE = 1024

class MyServer(socketserver.BaseRequestHandler):
    def handle(self):
        print("hello %s" % self.request)
        while True:
            cmd = self.request.recv(BUFSIZE)
            if len(cmd) == 0: break

            res = subprocess.Popen(cmd, shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
            stderr = res.stderr.read()
            stdout = res.stdout.read()
            if stderr:
                send_mes = stderr
            else:
                send_mes = stdout

            res_heads = {'data_len': len(send_mes)}
            res_heads_json = json.dumps(res_heads).encode('utf-8')

            self.request.send(struct.pack('i', len(res_heads_json)))
            self.request.send(res_heads_json)
            self.request.sendall(send_mes)

if __name__ == '__main__':

    socketserver.TCPServer.allow_reuse_address = True
    server = socketserver.ThreadingTCPServer((IP, PORT), MyServer)
    server.serve_forever()

客户端

import socket
import struct
import json

ip_port = ('127.0.0.1', 9999)
BUFSIZE = 1024

tcp_sk_client = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
tcp_sk_client.connect_ex(ip_port)

while True:
    cmd = input(">>: ").strip()
    if len(cmd) == 0: continue
    if cmd == 'quit': break

    tcp_sk_client.send(cmd.encode('utf-8'))

    # 接收 head 长度信息
    head_length = struct.unpack('i', tcp_sk_client.recv(4))[0]
    res_heads = json.loads(tcp_sk_client.recv(head_length).decode('utf-8'))

    cmd_res = b''
    recv_length = 0
    while recv_length < res_heads['data_len']:
        cmd_res += tcp_sk_client.recv(BUFSIZE)
        recv_length += BUFSIZE

    print(cmd_res.decode('utf-8'))

tcp_sk_client.close()

上述示例中通过一个字典(dict)来标识数据的一些信息(例如长度),发送端在发送数据时,先发送这个字典的长度,再发送这个字典(字典中携带了数据长度),最后发送数据信息~
 
.................^_^

上一篇:远程-粘包现象


下一篇:TensorFlow中的通信机制——Rendezvous(一)本地传输