Python基础-网络编程

第1章 Socket初使用

1.1 TCP和UDP协议

1.1.1 TCP协议

TCP(Transmission Control Protocol)可靠的、面向连接的协议(eg:打电话)、传输效率低全双工通信(发送缓存&接收缓存)、面向字节流。使用TCP的应用:Web浏览器;电子邮件、文件传输程序。

1.1.2 UDP协议

UDP(User Datagram Protocol)不可靠的、无连接的服务,传输效率高(发送前时延小),一对一、一对多、多对一、多对多、面向报文,尽最大努力服务,无拥塞控制。使用UDP的应用:域名系统 (DNS);视频流;IP语音(VoIP)。

1.1.3 TCP和UDP连接图示

图片[1]|Python基础-网络编程|leon的博客

1.2 socket介绍

1.2.1 socket参数的详解

socket.socket(family=AF_INET,type=SOCK_STREAM,proto=0,fileno=None)

创建socket对象的参数说明:

family 地址系列应为AF_INET(默认值),AF_INET6,AF_UNIX,AF_CAN或AF_RDS。

(AF_UNIX 域实际上是使用本地 socket 文件来通信)

type 套接字类型应为SOCK_STREAM(默认值),SOCK_DGRAM,SOCK_RAW或其他SOCK_常量之一。

SOCK_STREAM 是基于TCP的,有保障的(即能保证数据正确传送到对方)面向连接的SOCKET,多用于资料传送。

SOCK_DGRAM 是基于UDP的,无保障的面向消息的socket,多用于在网络上发广播信息。

proto 协议号通常为零,可以省略,或者在地址族为AF_CAN的情况下,协议应为CAN_RAW或CAN_BCM之一。
fileno 如果指定了fileno,则其他参数将被忽略,导致带有指定文件描述符的套接字返回。

与socket.fromfd()不同,fileno将返回相同的套接字,而不是重复的。

这可能有助于使用socket.close()关闭一个独立的插座。

1.2.2 socket方法介绍

服务端套接字函数
s.bind()    绑定(主机,端口号)到套接字
s.listen()  开始TCP监听
s.accept()  被动接受TCP客户的连接,(阻塞式)等待连接的到来

客户端套接字函数
s.connect()     主动初始化TCP服务器连接
s.connect_ex()  connect()函数的扩展版本,出错时返回出错码,而不是抛出异常

公共用途的套接字函数
s.recv()            接收TCP数据
s.send()            发送TCP数据
s.sendall()         发送TCP数据
s.recvfrom()        接收UDP数据
s.sendto()          发送UDP数据
s.getpeername()     连接到当前套接字的远端的地址
s.getsockname()     当前套接字的地址
s.getsockopt()      返回指定套接字的参数
s.setsockopt()      设置指定套接字的参数
s.close()           关闭套接字

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

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

1.2.3 send和sendall的区别

  • send()的返回值是发送的字节数量,这个数量值可能小于要发送的string的字节数,也就是说可能无法发送string中所有的数据。如果有错误则会抛出异常。
  • sendall()尝试发送string的所有数据,成功则返回None,失败则抛出异常。

1.3 基于TCP的Socket

Tips:tcp是基于链接的,必须先启动服务端,然后再启动客户端去链接服务端。

【服务端】:简单应用

import socket
sk = socket.socket()
sk.bind(('127.0.0.1',8080))

sk.listen()
conn,addr = sk.accept()
print(conn)
print(addr)

conn.send(b'hello')
msg = conn.recv(1024)
print(msg)

conn.close()
sk.close()

【客户端】:

import socket
sk = socket.socket()
sk.connect(('127.0.0.1',8080))

msg = sk.recv(1024)
print(msg)

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

【运行结果】:

服务端:
<socket.socket fd=528, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 5469)>
('127.0.0.1', 5469)
b'hi'

客户端:
b'hello'

【服务端】:时间服务器

import time
import socket

sk = socket.socket(type=socket.SOCK_DGRAM)
sk.bind(('127.0.0.1',8080))

while True:
    msg,addr = sk.recvfrom(1024)
    # msg 客户端发送给server端的时间格式 "%Y-%m-%d %H:%M-%S"
    time_format = msg.decode('utf-8')
    time_str = time.strftime(time_format)
    sk.sendto(time_str.encode('utf-8'),addr)
sk.close

【客户端】:

import socket

sk = socket.socket(type=socket.SOCK_DGRAM)
sk.sendto('%Y-%m-%d %H:%M:%S'.encode('utf-8'),('127.0.0.1',8080))

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

sk.close()

【运行结果】:

2018-10-13 11:41:07

【服务端】:基于TCP的聊天程序

import socket

sk = socket.socket()
sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
sk.bind(('127.0.0.1',8080))
sk.listen()

while True:
    conn,addr = sk.accept()
    while True:
        inp = input('>>>')
        if inp == 'q':
            conn.send(inp.encode('utf-8'))
            break
        conn.send(inp.encode('utf-8'))
        msg = conn.recv(1024)
        if msg == b'q':break
        print(msg.decode('utf-8'))
    conn.close()
sk.close()

【客户端】:

import socket
sk = socket.socket()
sk.connect(('127.0.0.1',8080))

while True:
    msg = sk.recv(1024)
    print(msg.decode('utf-8'))
    if msg == b'q':break
    inp = input('>>>')
    if inp == 'q':
        sk.send(inp.encode('utf-8'))
        break
    sk.send(inp.encode('utf-8'))
sk.close()

【运行结果】:

你好
>>>你也好
再见
>>>再见
q

1.4 基于UDP的Socket

Tips:udp是无链接的,启动服务之后可以直接接受消息,不需要提前建立链接。

【服务端】:简单应用

import socket
sk = socket.socket(type=socket.SOCK_DGRAM)
sk.bind(('127.0.0.1',8080))

msg,addr = sk.recvfrom(1024)
print(msg,addr)

sk.sendto(b'HELLO',addr)

sk.close()

【客户端】:

import socket

sk = socket.socket(type=socket.SOCK_DGRAM)
sk.sendto(b'hi',('127.0.0.1',8080))

msg,addr = sk.recvfrom(1024)
print(msg)

sk.close()

【运行结果】:

服务端:
b'hi' ('127.0.0.1', 64683)

客户端:
b'HELLO'

【服务端】:基于UDP的聊天程序

import socket
sk = socket.socket(type=socket.SOCK_DGRAM)
sk.bind(('127.0.0.1',8080))

while True:
    msg,addr = sk.recvfrom(1024)
    print('来自[%s:%s]的消息--%s'%(addr[0],addr[1],msg.decode('utf-8')))

    inp = input('>>>')
    sk.sendto(inp.encode('utf-8'),addr)
sk.close()

【客户端】:

import socket
sk = socket.socket(type=socket.SOCK_DGRAM)
addr = ('127.0.0.1',8080)

while True:
    msg = input('>>>')
    sk.sendto(msg.encode('utf-8'),addr)
    msg_recv,addr = sk.recvfrom(1024)
    print(msg_recv.decode('utf-8'))
sk.close()

【运行结果】:

服务端:
来自[127.0.0.1:64685]的消息--我是client1
>>>client1你好
来自[127.0.0.1:64686]的消息--我是client2
>>>client2你好
来自[127.0.0.1:64685]的消息--再见
>>>好的,再见
来自[127.0.0.1:64686]的消息--byebye
>>>bye

客户端1:
>>>我是client1
client1你好
>>>再见
好的,再见

客户端2:
>>>我是client2
client2你好
>>>byebye
bye

第2章 黏包

2.1 什么是黏包

网络传输中同时执行多条命令之后,得到的结果很可能只有一部分,在执行其他命令的时候又接收到之前执行的另外一部分结果,这种显现就是黏包。

2.2 黏包成因

2.2.1 tcp协议的拆包机制:

当发送端缓冲区的长度大于网卡的MTU时,tcp会将这次发送的数据拆成几个数据包发送出去。

MTU是Maximum Transmission Unit的缩写。意思是网络上传送的最大数据包。MTU的单位是字节。 大部分网络设备的MTU都是1500。如果本机的MTU比网关的MTU大,大的数据包就会被拆开来传送,这样会产生很多数据包碎片,增加丢包率,降低网络速度。

2.2.2 面向流的通信特点和Nagle算法

TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。

收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。

这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。

对于空消息:tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),也可以被发送,udp协议会帮你封装上消息头发送过去。

可靠黏包的tcp协议:tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。

2.2.3 基于tcp协议特点的黏包现象成因

例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束。

此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。

Tips:UDP不会发生黏包。

2.3 会发生黏包的两种情况

2.3.1 发送方的缓存机制

发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)

【服务端】:

from socket import *

ip_port = ('127.0.0.1',8080)

sk = socket()
sk.bind(ip_port)
sk.listen(5)

conn,addr = sk.accept()

data1 = conn.recv(10)
data2 = conn.recv(10)

print('----->',data1.decode('utf-8'))
print('----->',data2.decode('utf-8'))
conn.close()

【客户端】:

import socket

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

sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res = sk.connect_ex(ip_port)

sk.send('hello'.encode('utf-8'))
sk.send('hi'.encode('utf-8'))

【运行结果】:

-----> hellohi
----->

2.3.2 接收方的缓存机制

接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)

【服务端】:

from socket import *
import time

ip_port=('127.0.0.1',8080)

sk=socket()
sk.bind(ip_port)
sk.listen(5)

conn,addr=sk.accept()

data1=conn.recv(2)
data2=conn.recv(10)
print('----->',data1.decode('utf-8'))
print('----->',data2.decode('utf-8'))

time.sleep(1)
data2=conn.recv(10)
print('----->',data2.decode('utf-8'))

conn.close()
sk.close()

【客户端】:

import time
import socket

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

sk=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=sk.connect_ex(ip_port)

sk.send('hello'.encode('utf-8'))
time.sleep(1)
sk.send('hi'.encode('utf-8'))

【运行结果】:

-----> he
-----> llo
-----> hi

2.4 黏包总结

  • 黏包现象只发生在tcp协议中;
  • 从表面上看,黏包问题主要是因为发送方和接收方的缓存机制、tcp协议面向流通信的特点;
  • 实际上,主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。

2.5 黏包的解决方案

2.5.1 手动解决方案(不推荐)

问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据。

图片[2]|Python基础-网络编程|leon的博客

【服务端】:

import socket
import subprocess

ip_port=('127.0.0.1',8080)
sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

sk.bind(ip_port)
sk.listen(5)

while True:
    conn,addr=sk.accept()
    print('客户端',addr)
    while True:
        msg=conn.recv(1024)
        if not msg:break
        res=subprocess.Popen(msg.decode('utf-8'),shell=True,\
                            stdin=subprocess.PIPE,\
                            stderr=subprocess.PIPE,\
                            stdout=subprocess.PIPE)

        err=res.stderr.read()
        if err:
            ret=err
        else:
            ret=res.stdout.read()

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

        data=conn.recv(1024).decode('utf-8')
        if data == 'recv_ready':
            conn.sendall(ret)

    conn.close()

【客户端】:

import socket

sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=sk.connect_ex(('127.0.0.1',8080))

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

    sk.send(msg.encode('utf-8'))
    length=int(sk.recv(1024).decode('utf-8'))
    sk.send('recv_ready'.encode('utf-8'))

    send_size=0
    recv_size=0
    data=b''

    while recv_size < length:
        data += sk.recv(1024)
        recv_size += len(data)

    print(data.decode('gbk'))

【运行结果】:

>>: ipconfig

Windows IP 配置

以太网适配器 以太网:

连接特定的 DNS 后缀 . . . . . . . :
本地链接 IPv6 地址. . . . . . . . : fe80::d4b8:fb07:a675:b5a1%16
IPv4 地址 . . . . . . . . . . . . : 192.168.2.6
子网掩码  . . . . . . . . . . . . : 255.255.255.0
默认网关. . . . . . . . . . . . . : 192.168.2.1

无线局域网适配器 WLAN:

媒体状态  . . . . . . . . . . . . : 媒体已断开连接
连接特定的 DNS 后缀 . . . . . . . :

以太网适配器 以太网 2:

媒体状态  . . . . . . . . . . . . : 媒体已断开连接
连接特定的 DNS 后缀 . . . . . . . :

>>: dir
驱动器 D 中的卷是 工作
卷的序列号是 5EA1-E7AF

D:\WorkSpace\Python\python_study\14.网络编程\黏包\手动解决方案 的目录

2018/10/13 周六  11:17    <DIR>          .
2018/10/13 周六  11:17    <DIR>          ..
2018/10/13 周六  11:17               785 client.py
2018/10/13 周六  11:07             1,182 server.py
2 个文件          1,967 字节
2 个目录 599,890,804,736 可用字节

>>: ls
'ls' 不是内部或外部命令,也不是可运行的程序
或批处理文件。

>>: pwd
'pwd' 不是内部或外部命令,也不是可运行的程序
或批处理文件。

>>: quit

Tips:存在的问题:

程序的运行速度远快于网络传输速度,所以在发送一段字节前,先用send去发送该字节流长度,这种方式会放大网络延迟带来的性能损耗。

2.5.2 struct模块方案(推荐方法)

struct这个模块可以把要发送的数据长度转换成固定长度的字节,这样客户端每次接收消息之前只要先接受这个固定长度字节的内容看一看接下来要接收的信息大小,那么最终接受的数据只要达到这个值就停止,就能刚好不多不少的接收完整的数据了。

Tips:该模块可以把一个类型,如数字,转成固定长度的bytes。

  • 方法一:
发送时 接收时
先发送struct转换好的数据长度4字节 先接受4个字节使用struct转换成数字来获取要接收的数据长度
再发送数据 再按照长度接收数据

【服务端】:

import socket
import struct
import subprocess

sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)  # 就是它,在bind前加

sk.bind(('127.0.0.1',8080))
sk.listen(5)

while True:
    conn,addr=sk.accept()
    while True:
        cmd=conn.recv(1024)
        if not cmd:break
        print('cmd: %s' %cmd)

        res=subprocess.Popen(cmd.decode('utf-8'),
                             shell=True,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE)

         err=res.stderr.read()
         print(err)
         if err:
             back_msg=err
         else:
             back_msg=res.stdout.read()

         conn.send(struct.pack('i',len(back_msg)))       # 先发back_msg的长度
         conn.sendall(back_msg)                          # 再发真实的内容

    conn.close()

【客户端】:

import socket
import struct

sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=sk.connect_ex(('127.0.0.1',8080))

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

    sk.send(msg.encode('utf-8'))

    l=sk.recv(4)
    x=struct.unpack('i',l)[0]
    print(type(x),x)
    # print(struct.unpack('I',l))
    r_s=0
    data=b''

    while r_s < x:
        r_d=sk.recv(1024)
        data+=r_d
        r_s+=len(r_d)

    # print(data.decode('utf-8'))   # Linux默认UTF-8编码
    print(data.decode('gbk'))       # Windows默认GBK编码

【运行结果】:

服务端:
cmd: b'dir'
b''
cmd: b'ls'
b"'ls' \xb2\xbb\xca\xc7\xc4\xda\xb2\xbf\xbb\xf2\xcd\xe2\xb2\xbf\xc3\xfc\xc1\xee\xa3\xac\xd2\xb2\xb2\xbb\xca\xc7\xbf\xc9\xd4\xcb\xd0\xd0\xb5\xc4\xb3\xcc\xd0\xf2\r\n\xbb\xf2\xc5\xfa\xb4\xa6\xc0\xed\xce\xc4\xbc\xfe\xa1\xa3\r\n"

客户端:
>>: dir
<class 'int'> 412
驱动器 D 中的卷是 工作
卷的序列号是 5EA1-E7AF

D:\WorkSpace\Python\python_study\14.网络编程\黏包\struct方案 的目录

2018/10/13 周六  11:30    <DIR>          .
2018/10/13 周六  11:30    <DIR>          ..
2018/10/13 周六  11:30               887 client.py
2018/10/13 周六  11:30             1,189 server.py
2 个文件          2,076 字节
2 个目录 599,890,792,448 可用字节

>>: ls
<class 'int'> 61
'ls' 不是内部或外部命令,也不是可运行的程序
或批处理文件。
  • 方法二:

把报头做成字典,字典里包含将要发送的真实数据的详细信息,然后json序列化,然后用struck将序列化后的数据长度打包成4个字节(4个自己足够用了)

发送时 接收时
先发报头长度 先收报头长度,用struct取出来
再编码报头内容然后发送 根据取出的长度收取报头内容,然后解码,反序列化
最后发真实内容 从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容

【服务端】:

import socket
import struct
import json
import subprocess

sk=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)  # 就是它,在bind前加

sk.bind(('127.0.0.1',8080))
sk.listen(5)

while True:
    conn,addr=sk.accept()
    while True:
        cmd=conn.recv(1024)
        if not cmd:break
        print('cmd: %s' %cmd)

        res=subprocess.Popen(cmd.decode('utf-8'),
                             shell=True,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE)

        err=res.stderr.read()
        print(err)
        if err:
            back_msg=err
        else:
            back_msg=res.stdout.read()

        headers={'data_size':len(back_msg)}
        head_json=json.dumps(headers)
        head_json_bytes=bytes(head_json,encoding='utf-8')

        conn.send(struct.pack('i',len(head_json_bytes)))    # 先发报头的长度
        conn.send(head_json_bytes)                          # 再发报头
        conn.sendall(back_msg)                              # 在发真实的内容

    conn.close()

【客户端】:

from socket import *
import struct
import json

ip_port=('127.0.0.1',8080)
sk = socket(AF_INET,SOCK_STREAM)
sk.connect(ip_port)

while True:
    cmd=input('>>: ')
    if not cmd:continue
    sk.send(bytes(cmd,encoding='utf-8'))

    head=sk.recv(4)
    head_json_len=struct.unpack('i',head)[0]
    head_json=json.loads(sk.recv(head_json_len).decode('utf-8'))
    data_len=head_json['data_size']

    recv_size=0
    recv_data=b''

    while recv_size < data_len:
        recv_data+=sk.recv(1024)
        recv_size+=len(recv_data)

    #print(recv_data.decode('utf-8'))
    print(recv_data.decode('gbk'))      # windows默认gbk编码

【运行结果】:

服务端:
cmd: b'dir'
b''
cmd: b'ls'
b"'ls' \xb2\xbb\xca\xc7\xc4\xda\xb2\xbf\xbb\xf2\xcd\xe2\xb2\xbf\xc3\xfc\xc1\xee\xa3\xac\xd2\xb2\xb2\xbb\xca\xc7\xbf\xc9\xd4\xcb\xd0\xd0\xb5\xc4\xb3\xcc\xd0\xf2\r\n\xbb\xf2\xc5\xfa\xb4\xa6\xc0\xed\xce\xc4\xbc\xfe\xa1\xa3\r\n"

客户端:
>>: dir
驱动器 D 中的卷是 工作
卷的序列号是 5EA1-E7AF

D:\WorkSpace\Python\python_study\14.网络编程\黏包\struct方案二 的目录

2018/10/13 周六  11:35    <DIR>          .
2018/10/13 周六  11:35    <DIR>          ..
2018/10/13 周六  11:35               925 client.py
2018/10/13 周六  11:35             1,423 server.py
2 个文件          2,348 字节
2 个目录 599,890,784,256 可用字节

>>: ls
'ls' 不是内部或外部命令,也不是可运行的程序
或批处理文件。

2.5.3 FTP服务器示例

【服务端】:

# IP port 写在配置文件中

import json
import socket
import struct

buffer = 1024
sk = socket.socket()
sk.bind(('127.0.0.1',8090))
sk.listen()

conn,addr = sk.accept()

# 接收
head_len = conn.recv(4)
head_len = struct.unpack('i',head_len)[0]
json_head = conn.recv(head_len).decode('utf-8')
head = json.loads(json_head)

filesize = head['filesize']
with open(head['filename'],'wb') as f:
    while filesize:
        if filesize >= buffer:
            content = conn.recv(buffer)
            f.write(content)
            filesize -= buffer
        else:
            content = conn.recv(filesize)
            f.write(content)
            break

conn.close()
sk.close()

【客户端】:

import os
import json
import socket
import struct

buffer = 1024
sk = socket.socket()
sk.connect(('127.0.0.1',8090))

head = {'filepath':r'Z:\day32',
        'filename':r'04 python fullstack s9day32 struct模块补充.mp4',
        'filesize':None}
file_path = os.path.join(head['filepath'],head['filename'])
filesize = os.path.getsize(file_path)
head['filesize'] = filesize

json_head = json.dumps(head)
bytes_head = json_head.encode('utf-8')
# print(json_head)
# print(bytes_head)

# 计算head长度
head_len = len(bytes_head)      # 报头的长度
pack_len = struct.pack('i',head_len)

sk.send(pack_len)               # 先发报头长度
sk.send(bytes_head)             # 再发bytes类型报头

with open(file_path,'rb') as f:
    while filesize:
        if filesize >= buffer:
            content = f.read(buffer)
            sk.send(content)
            filesize -= buffer
        else:
            content = f.read(filesize)
            sk.send(content)
            break
sk.close()

【运行结果】:

图片[3]|Python基础-网络编程|leon的博客

第3章 socket扩展内容

3.1 验证客户端链接的合法性

如果你想在分布式系统中实现一个简单的客户端链接认证功能,又不像SSL那么复杂,那么利用hmac+加盐的方式来实现。

【服务端】:

import os
import socket
import hmac

def check_client(conn):
    secret_key = b'leon'                # 密钥
    send_str = os.urandom(32)
    conn.send(send_str)
    hmac_obj = hmac.new(secret_key,send_str)
    secret_ret = hmac_obj.digest()      # bytes类型
    if conn.recv(1024) == secret_ret:
        print('合法的客户端')
        return True
    else:
        print('非法的客户端')
        return False

sk = socket.socket()
sk.bind(('127.0.0.1',8090))
sk.listen()

conn,addr = sk.accept()
ret = check_client(conn)
while ret:
    inp = input('>>>')
    conn.send(inp.encode('utf-8'))
    msg = conn.recv(1024)
    print(msg.decode('utf-8'))
conn.close()
sk.close()

【客户端】:

import socket
import hmac

sk = socket.socket()
sk.connect(('127.0.0.1',8090))

recv = sk.recv(1024)
# 用和server端相同的手法对这个字符串进行摘要
secret_key = b'leon'        # 密钥
hmac_obj = hmac.new(secret_key,recv)
ret = hmac_obj.digest()
sk.send(ret)
msg = sk.recv(1024)
if msg:
    print(msg.decode('utf-8'))
    while True:
        inp = input('>>>')
        sk.send(inp.encode('utf-8'))
        msg = sk.recv(1024)
        print(msg.decode('utf-8'))
sk.close()

【运行结果】:

服务端:
合法的客户端
>>>hi
hello
>>>bye
bye

客户端:
hi
>>>hello
bye

3.2 socketserver模块介绍

3.2.1 socketserver模块介绍

socketserver内部使用 IO多路复用以及 “多线程” 和 “多进程”,从而实现并发处理多个客户端请求的Socket服务端。即:每个客户端请求连接到服务器时,Socket服务端都会在服务器是创建一个“线程”或者“进程” 专门负责处理当前客户端的所有请求。

3.2.2 socketserver模块源码分析

http://www.cnblogs.com/Eva-J/p/5081851.html

3.2.3 程序示例

【服务端】:

import json
import hashlib
import socketserver

def md5_pwd(user,pwd):
    md5_obj = hashlib.md5(user.encode('utf-8'))
    md5_obj.update(pwd.encode('utf-8'))
    ret = md5_obj.hexdigest()
    return ret

def login(userinfo):
    user_dic = json.loads(userinfo)
    passwd = md5_pwd(user_dic['username'], user_dic['passwd'])
    with open('userinfo') as f:
        for line in f:
            user,pwd = line.split('|')
            if user_dic['username'] == user and passwd == pwd:
                print('登录成功')
                break

class MyServer(socketserver.BaseRequestHandler):
    def handle(self):
        userinfo = self.request.recv(1024).decode('utf-8')    # self.request == conn
        login(userinfo)

server = socketserver.ThreadingTCPServer(
                ('127.0.0.1',9000),
                MyServer)
server.serve_forever()

【客户端】:

import json
import socket

ADDR = ('127.0.0.1',9000)

def get_socket():
    sk = socket.socket()
    sk.connect(ADDR)
    return sk

# 输入账号
username = input('username >>>')
passwd = input('password >>>')
if username.strip() and passwd.strip():
    sk = get_socket()
    dic = {'username':username,'passwd':passwd}
    str_dic = json.dumps(dic)
    sk.send(str_dic.encode('utf-8'))

sk.close()

【配置文件】:

userinfo:
leon|68b2326123681498f854db9495170ac0

【运行结果】:

客户端:
username >>>leon
password >>>123456

服务端:
登录成功
温馨提示:本文最后更新于2022-12-20 20:57:46,已超过491天没有更新。某些文章具有时效性,若文章内容或图片资源有错误或已失效,请联系站长。谢谢!
转载请注明本文链接:https://blog.leonshadow.cn/763482/1284.html
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享