3.1.2 Socket网络通信开发

Socket语法

Python中,我们用Socket()函数来创建套接字,语法如下:

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

参数

  • family:套接字家族可以使用AF_UNIX或者AF_INET
  • type:套接字类型可以根据是面向连接的还是非连接分为SOCK_STREAM或SOCK_DGRAM
  • protocol:一般不填默认为0

Socket对象方法

函数 描述
服务器端套接字  
s.bind() 绑定地址(host, port)到套接字, 在AF_INET下,以元组(host, port)的形式表示地址
s.listen() 开始TCP监听。backlog指定在拒绝连接之前,操作系统可以挂起的最大连接数量。该值至少为1,大部分应用程序设为5就可以了
s.accept() 被动接受TCP客户端连接,(阻塞式)等待连接的到来
客户端套接字  
s.connect() 主动初始化TCP服务器连接。一般address的格式为元组(hostname, port),如果连接出错,返回socket.error错误
s.connect_ex() connect()函数的扩展版本,出错时返回出错码,而不是抛出异常
公用套接字  
s.recv() 接收TCP数据,数据以字符串形式返回,bufsize指定要接收的最大数据量。flag提供有关消息的其他信息,通常可以忽略
s.send() 发送TCP数据,将string中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于string的字节大小
s.sendall() 完整发送TCP数据。将string中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常
s.recvfrom() 接收UDP数据,与recv()类似,但返回值是(data,address)。其中data是包含接收数据的字符串,address是发送数据的套接字地址
s.sendto() 发送UDP数据,将数据发送到套接字,address是形式为(ipaddr, port)的元组,指定远程地址。返回值是发送的字节数
s.close() 关闭套接字
s.getpeername() 返回连接套接字的远程地址。返回值通常是元组(ipaddr, port)
s.getsockname() 返回套接字自己的地址。通常是一个元组(ipaddr, port)
s.setsockopt(level, optname, value) 设置给定套接字选项的值
s.getsockopt(level, optname[bullen]) 返回套接字选项的值
s.settimeout(timeout) 设置套接字操作的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如connect())
s.gettimeout() 返回当前超时期的值,单位是秒,如果没有设置超时期,则返回None
s.fileno 返回套接字的文件描述符
s.setblocking(flag) 如果flag为0,则将套接字设为非阻塞模式,否则将套接字设为阻塞模式(默认值)。非阻塞模式下,如果调用recv()没有发现任何数据,或send()调用无法立即发送数据,那么将引起socket.error异常
s.makefile() 创建一个与该套接字相关连的文件

 

Socket简单实例

服务器端

import socket

server = socket.socket()    #生成一个socket实例对象
HOST = socket.gethostname()    #获取主机名
PORT = 9999
server.bind((HOST, PORT))    #绑定主机名和端口
server.listen(5)    #监听
print('开始监听...')

conn, addr = server.accept()    #等待连接,conn就是客户端连接过来在服务器端生成的连接实例
print(conn, addr)
data = conn.recv(1024)    #设定接收数据大小,单位为字节
print('recv: ', data)
conn.send(data.upper())    #将接收到的数据大写后再发回

server.close()

客户端

import socket

client = socket.socket()    #生成一个套接字对象
HOST = socket.gethostname()    #获取本机主机名
PORT = 9999
client.connect((HOST, PORT))

client.send(b'Hello World!')    #发送数据,在python3.x中,只能发送bytes类型的数据
data = client.recv(1024)         #设定接收数据的大小,单位是字节
print('recv: ', data)

client.close()

服务器端输出

开始监听...
 ('192.168.1.3', 57354)
recv:  b'Hello World!'

客户端输出

recv:  b'HELLO WORLD!'

但上面的示例有很多问题。

首先,它是写死的,每运行一次只能发送一条消息,并且消息也是固定的。

其次,它同时只能跟一个客户端进行通信,在实际生产中,这种程序基本没用。

我们先来解决第一个问题,改进上面的示例,让它可以多次收发数据。

服务器端

import socket

server = socket.socket()    #生成一个socket实例对象
HOST = socket.gethostname()    #获取主机名
PORT = 9999
server.bind((HOST, PORT))    #绑定主机名和端口
server.listen(5)    #监听
print('开始监听...')

while True:    #此循环可以让服务器端可以不断的等待接收新链接
    conn, addr = server.accept()    #等待连接,conn就是客户端连接过来在服务器端生成的连接实例
    print(conn, addr)
    while True:    #此循环让服务器端可以不断的接收新数据
        data = conn.recv(1024)    #设定接收数据大小,单位为字节
        print('recv: ', data)
        if not data:
            print('client has lost...')
            break
        conn.send(data.upper())    #将接收到的数据大写后再发回

客户端

import socket

client = socket.socket()    #生成一个套接字对象
HOST = socket.gethostname()    #获取本机主机名
PORT = 9999
client.connect((HOST, PORT))

while True:    #加一个循环,让客户端可以不断的发送数据
    msg = input('>>> ').strip()
    if len(msg) == 0:    #不加这个判断,发送数据为空,程序会卡住
        continue
    client.send(msg.encode('utf-8'))    #发送数据,在python3.x中,只能发送bytes类型的数据
    data = client.recv(1024)         #设定接收数据的大小,单位是字节
    print('recv: ', data)

此时该示例程序不仅可以实现单个链接反复收发消息,还允许多个链接挂起等待。需在windows命令行下实现,在pycharm中当结束第一个客户端链接时,客户端和服务器端会同时报错,但在cmd命令行中则不会。

步骤1

如下图所示,启动服务器端后,挂起两个链接,可以看到,第一个链接已经接入。

3.1.2 Socket网络通信开发_第1张图片

链接一开始和服务器端互动,互动结束按ctrl+c,链接2自动接入并等待收发信息

3.1.2 Socket网络通信开发_第2张图片

链接2与服务器端互动

3.1.2 Socket网络通信开发_第3张图片

要想实现服务器同时与多个客户端同时连接并持续收发消息,须掌握异步相关知识。目前只能做到同时与一个客户端连接并持续收发消息。

上面的示例还有个问题,关于recv()接收大小的问题。比如下面这个简单的ssh示例:

收发大数据

先看下面的示例

服务器端

import socket
import os

server = socket.socket()
host = socket.gethostname()
port = 9999
server.bind((host, port))
print(host)
server.listen(5)

while True:
    conn, addr = server.accept()
    print('new conn: ', addr)
    while True:
        print('waiting new orders...')
        data = conn.recv(1024)
        if not data:
            print('客户端已断开')
            break
        print('执行命令: ', data)
        cmd_res = os.popen(data.decode()).read()    #2接收到数据后解码
        print('before send', len(cmd_res))
        if len(cmd_res) == 0:
            cmd_res = 'cmd has no output...'
        conn.send(cmd_res.encode('utf-8'))    #3再发回时依然需要编码
        print('send done!')

server.close()

客户端

import socket

client = socket.socket()
host = socket.gethostname()
port = 9999
client.connect((host, port))

while True:
    cmd = input('\>: ').strip()
    if len(cmd) == 0:
        continue
    client.send(cmd.encode('utf-8'))    #1未编码的数据为str类型,无法发送
    cmd_res = client.recv(1024)
    print(cmd_res.decode())    #4再解码得到结果

client.close()

运行结果如下

3.1.2 Socket网络通信开发_第4张图片

上面2个窗口分别是server端和client端,并在client端输出tasklist命令结果。第三个窗口是操作系统直接输出tasklist命令的结果。我们可以看到2个结果对比,明显不一样。client端的结果只显示了10来条就结束了,并且收到的最后一条也不完整。为什么会出现这个问题?

答案在于recv()函数,recv()函数接收到的数值定义了接收值的大小。程序中设定的是1024,即1024字节,也就是我们常说的1kb。上面这个程序在运行dir等输出结果小于1024字节的命令时不会出错,但当输出结果大于1024时,系统会将大于1024字节的部分暂存在系统的缓冲区里,在下一次客户端与服务器端交互时释放。

很明显,这不是我们要的效果。我们要的是输入一个命令,它能够一次返回所有的输出结果。那么,我们要如何解决这个问题呢?

答案就是在发送前服务器计算要发送的数据长度,将这个值发送到客户端,开始循环接收,只要接收长度与数据实际长度不相等就一直接收。然后结束。

服务器端

import socket
import os

server = socket.socket()
host = socket.gethostname()
port = 9999
server.bind((host, port))
print(host)
server.listen(5)

while True:
    conn, addr = server.accept()
    print('new conn: ', addr)
    while True:
        print('waiting new orders...')
        data = conn.recv(1024)
        if not data:
            print('客户端已断开')
            break
        print('执行命令: ', data)
        cmd_res = os.popen(data.decode()).read()    #2接收到数据后解码
        print('before send', len(cmd_res))
        if len(cmd_res) == 0:
            cmd_res = 'cmd has no output...'
        conn.send(str(len(cmd_res.encode('utf-8'))).encode('utf-8'))
        #第一个encode(): 数据长度必须转码,否则客户端接收长度与数据实际长度不一致
        #第二个encode(): len数据类型无法转码,必须先转换成str类型
        conn.send(cmd_res.encode('utf-8'))    #3再发回时依然需要编码
        print('send done!')

server.close()

客户端

import socket

client = socket.socket()
host = socket.gethostname()
port = 9999
client.connect((host, port))

while True:
    cmd = input('\>: ').strip()
    if len(cmd) == 0:
        continue
    client.send(cmd.encode('utf-8'))    #1未编码的数据为str类型,无法发送
    cmd_size = client.recv(1024)    #接收命令结果的长度
    print('length of cmd_res: ', cmd_size)
    received_size = 0

    while received_size != int(cmd_size.decode()):    #只要接收值和数据值不相等就一直收
        data = client.recv(1024)
        received_size += len(data)    #计算实际接收长度
        print(received_size)
        print(data.decode())    #4再解码得到结果
    else:
        print('Transmission has been done!', received_size)

client.close()

运行结果

3.1.2 Socket网络通信开发_第5张图片

可以看到实际接收长度与数据长度一致后,程序自动进入下一个命令等待中。

怎么样,看着很完美了是吧。但以上程序还有个小问题,服务器端里有两行代码连续使用了send()。

conn.send(str(len(cmd_res.encode('utf-8'))).encode('utf-8'))
        #第一个encode(): 数据长度必须转码,否则客户端接收长度与数据实际长度不一致
        #第二个encode(): len数据类型无法转码,必须先转换成str类型
conn.send(cmd_res.encode('utf-8'))    #3再发回时依然需要编码

这里有可能会造成粘(nian2)包问题。2次数据合并成一次发送。如何解决呢?

服务器端

import socket
import os

server = socket.socket()
host = socket.gethostname()
port = 9999
server.bind((host, port))
print(host)
server.listen(5)

while True:
    conn, addr = server.accept()
    print('new conn: ', addr)
    while True:
        print('waiting new orders...')
        data = conn.recv(1024)
        if not data:
            print('客户端已断开')
            break
        print('执行命令: ', data)
        cmd_res = os.popen(data.decode()).read()    #2接收到数据后解码
        print('before send', len(cmd_res))
        if len(cmd_res) == 0:
            cmd_res = 'cmd has no output...'
        conn.send(str(len(cmd_res.encode('utf-8'))).encode('utf-8'))
        #第一个encode(): 数据长度必须转码,否则客户端接收长度与数据实际长度不一致
        #第二个encode(): len数据类型无法转码,必须先转换成str类型
        client_ack = conn.recv(1024)
        print('ack from client', client_ack)
        #添加这两行让服务器端和客户端多一次交互以解决粘包问题
        conn.send(cmd_res.encode('utf-8'))    #3再发回时依然需要编码
        print('send done!')

server.close()

客户端

import socket

client = socket.socket()
host = socket.gethostname()
port = 9999
client.connect((host, port))

while True:
    cmd = input('\>: ').strip()
    if len(cmd) == 0:
        continue
    client.send(cmd.encode('utf-8'))    #1未编码的数据为str类型,无法发送
    cmd_size = client.recv(1024)    #接收命令结果的长度
    print('length of cmd_res: ', cmd_size)
    client.send('ready to transmit'.encode('utf-8'))    #增加一次交互以解决粘包
    received_size = 0
    received_data = b''    #定义一个空数据
    while received_size != int(cmd_size.decode()):    #只要接收值和数据值不相等就一直收
        data = client.recv(1024)
        received_size += len(data)    #计算实际接收长度
        # print(received_size)
        # print(data.decode())    #4再解码得到结果
        received_data += data    #每次循环更新接收数据
    else:
        print('Transmission has been done!', received_size)
        print(received_data.decode())    #循环完毕输出数据

client.close()

运行结果就不贴了,因为粘包问题不是每次都会发生。但解决的方法务必要掌握。

到这里已经掌握了如何连续收发数据。

基于以上的内容,下面实现一个FTP,通常FTP分以下几个步骤:

  1. 读取文件名
  2. 检测文件名是否存在
  3. 打开文件
  4. 检测文件长度
  5. 发送长度给客户端
  6. 等待客户端确认(这一步是为了防止粘包)
  7. 开始边读边发送数据以及md5
  8. 传送完毕后与客户端接收到的最后的md5进行比对

下面是一个简单的FTP示例:

服务器端

import socket
import os
import hashlib

host = socket.gethostname()
port = 9999

server = socket.socket()
server.bind((host, port))
server.listen(5)

while True:
    conn, addr = server.accept()    #等待连接
    print('From Server: Connection established.', addr)
    while True:
        print('Waiting for new orders....')
        data = conn.recv(1024)    #接收命令
        if not data:
            print('From Server: Connection has failed.')
            break
        cmd, filename = data.decode().split()
        print('From Server: Filename is ', filename)
        if os.path.isfile(filename):
            f = open(filename, 'rb')
            m = hashlib.md5()
            file_size = os.stat(filename).st_size
            conn.send(str(file_size).encode('utf-8'))
            #发送文件长度,file_size是int类型,无法直接编码,须先转成str类型
            conn.recv(1024)    #等待客户端确认
            for line in f:
                m.update(line)    #逐行更新md5值
                conn.send(line)    #开始逐行发送数据
            print('From Server: MD5: ', m.hexdigest())    #文件发送完毕,md5更新完毕
            f.close()
            conn.send(m.hexdigest().encode())

        print('From Server: Transmission has been done!')

客户端

import socket
import hashlib

host = socket.gethostname()
port = 9999
client = socket.socket()
client.connect((host, port))    #发起连接

while True:
    cmd = input('\>:').strip()
    if len(cmd) == 0:
        continue
    if cmd.startswith('get'):
        client.send(cmd.encode('utf-8'))    #发送传送文件的命令
        server_response = client.recv(1024)    #接收文件长度
        print('From Client: Server response: ', server_response)
        client.send(b'Ready to transmit.')    #发送确认信息
        file_size = int(server_response.decode())
        #接收到的文件长度需要先解码然后转换成int类型
        received_size = 0
        filename = cmd.split()[1]
        f = open(filename + '_new', 'wb')
        m = hashlib.md5()

        while received_size < file_size:
            if file_size - received_size > 0:    #意为不止收一次
                size = 1024
            else:   #剩多少收多少
                size = file_size - received_size
                print('last receive: ', size)
            data = client.recv(size)    #每次接收的长度
            received_size += len(data)    #已收到长度随循环自增
            m.update(data)    #不断更新md5
            f.write(data)    #不断写入
            #print('From Client: File size: %d, Received size: %d' % (file_size, received_size))
        else:
            new_file_md5 = m.hexdigest()    #传送完毕后的md5
            print('From Client: Transmission has been done!')
            f.close()
        server_md5 = client.recv(1024)
        print('From Client: Client MD5: ', new_file_md5)
        print('From Server: Server MD5: ', server_md5)

输出结果:

服务器端

From Server: Connection established. ('192.168.1.3', 53230)
Waiting for new orders....
From Server: Filename is  uplayinstaller.exe
From Server: MD5:  955765b2cba4489a3639758aecab94a3
From Server: Transmission has been done!
Waiting for new orders....

客户端

\>:get uplayinstaller.exe
From Client: Server response:  b'103203256'
From Client: Transmission has been done!
From Client: Client MD5:  955765b2cba4489a3639758aecab94a3
From Server: Server MD5:  b'955765b2cba4489a3639758aecab94a3'
\>:

这个程序已经能实现简单的文件传输,但依然有很多不足的地方。比如,程序不能处理错误,传输文件只能在程序所在文件夹进行。比较成熟的FTP项目会在后面进行。

关于Socket的内容就写到这。

你可能感兴趣的:(3.1.2 Socket网络通信开发)