Python网络编程(小白一看就懂)

Python网络编程(小白一看就懂)

一、网络编程基础概念

1. mac地址与ip地址

举个例子,在大学校园,找到名叫Lucy的男神可以用学号,脱离了校园,你要找到Lucy男神,你就要通过身份证号,这里学号是临时的编号,变化的,出了校园就不可用了;身份证号是唯一标识,不变的,在哪里都能找到。

在计算机中,也有类似的概念,不变的是mac地址,能够唯一标识你这台机器;变化的是ip地址,能够更方便的找到你的机器。(可以类比生活中运送快递)

1.1 ip地址

ipv4协议:规定ip地址是由四位点分十进制数组成,每一位都是八位二进制(表示的范围是0 ~ 255),所以ip地址范围0.0.0.0 ~ 255.255.255.255

公网地址:需要我们自己申请购买的地址

内网地址:保留字段,192.168.0.0 ~ 192.168.255.255 和 172.16.0.0 ~ 172.31.255.255 和 10.0.0.0 ~ 10.255.255.255,在这些范围内的地址都是内网地址。

访问公网地址用的是路由器,访问内网地址用的是交换机。

有一个特殊的ip地址,127.0.0.1 本地回环地址。一般都是做测试的时候使用。

查看自己的ip地址:在cmd中输入ipconfig查看。linux和mac通过ifconfig。

比如,172.168.14.0这个网段内,最多能有254台机器,还有一台是网关ip。因为ip地址中0用于指定网络地址号,192.168.14.0代表的是网段,而不是IP。

子网掩码:也是一个ip地址,用来判断两台机器在不在一个局域网内。用其中一个ip地址和子网掩码进行按位与(先把每位都转成二进制,然后再按位与),在用另外一个ip地址和子网掩码进行按位与,然后比较两次按位与后的ip地址是否一样,如果一样,说明在同一个局域网,反之,不在同一个局域网。

比如192.168.12.1和192.168.13.1,前面只有2位一样(192.168),所以只需2个255,即子网掩码是255.255.0.0,能说明这两个ip在同一个局域网。

有两台机器,分别是192.168.12.1和192.168.13.1,如何判断这两台地址在同一个网段内
子网掩码是255.255.255.0-->11111111.11111111.11111111.00000000
192.168.12.1-->11000000.10101000.00000110.00000001
按位与后是11000000.10101000.00000110.00000000-->192.168.12.0

192.168.13.1-->11000000.10101000.00000111.00000001
按位与后是11000000.10101000.00000111.00000000-->192.168.13.0

按位与后的两个地址不同,说明不在一个局域网,反之,在同一个局域网。
上面例子说明不在同一个局域网。

1.2 mac地址

在网卡上,唯一。

arp协议:地址解析协议。通过一台机器的ip地址获取到它的mac地址,用到了交换机的广播和单播功能。

端口

端口的范围:0 ~ 65535,用来确认机器上的具体应用程序的。

2. 局域网

2.1 局域网内通信

连在同一台交换机上的多台机器形成了局域网络,所有的消息传递,都是交给交换机来处理。交换机只识别mac地址。

我知道一台机器的ip地址,然后要给它发消息,由于交换机不认识ip地址,所以要先获取到这台机器的mac地址,然后双方都知道对方的mac地址,这样用交换机进行数据的传递就非常方便了。

交换机:广播、单播、组播

网段:192.168.12.XX

2.2 局域网间通信

需要借助路由器。路由器认识ip地址。路由器提供网关ip,同一个局域网的所有机器共享一个网关,我们不能访问除了本局域网之外的其他内网的ip地址。

网关:一个网络连接到另一个网络的“关口”。

二、最简单的网络通信-socket实现的代码

server.py

import socket
# 套接字
sk = socket.socket()  # 创建一个server端的对象
sk.bind(('127.0.0.1', 9000))  # 给server端绑定一个地址
sk.listen()  # 开始监听客户端给我的连接

conn, adder = sk.accept()  # 建立连接,conn是连接

# 服务端 发送 消息给客户端
conn.send(b'hello')

# 服务端 接收 来自客户端的消息
msg = conn.recv(1024)
print(msg)
conn.close()  # 关闭连接

sk.close()

client.py

import socket

sk = socket.socket()

# 必须和server端的一致
sk.connect(('127.0.0.1', 9000))
msg = sk.recv(1024)
print(msg)

# 客户端 发送 消息给服务端
sk.send(b'bye')

sk.close()

三、tcp、udp、osi

七层协议:应用层、表示层、会话层、传输层、网络层、数据链路层、物理层

3.1 osi5层协议

3.1.1 理论

应用层:python代码,b’hello’

传输层:预备如何传输、使用的端口,b’发送者的端口+hello+接受者的端口’

网络层:ip,b’发送者的ip+发送者的端口+hello+接受者的端口+接受者的端口’

数据链路层:mac,b’发送者mac地址+发送者的ip+发送者的端口+hello+接受者的端口+接受者的端口+接受者mac地址’

物理层:转化成电信号,通过网线传输

3.1.2 每层涉及到的协议和物理设备

第五层 应用层:python代码。

第四层 传输层:port,udp,tcp。四层路由器,四层交换机。

第三层 网络层:ipv4和ipv6。路由器,三层交换机。(家里的路由器带有交换机功能)

第二层 数据链路层:mac,arp协议。网卡,二层交换机。

第一层 物理层:网线。

3.2 tcp协议和udp协议

3.2.1 tcp协议

tcp(语音聊天)- 线下缓存高清电影、qq远程控制、发邮件

需要先建立连接,然后才能通信的。

特点:占用连接、可靠(消息不会丢失)、实时性高、慢

建立连接:指三次握手

断开连接:指四次挥手

3.2.2 udp协议

udp(发短信)- 在线播放视频、qq发消息、微信发消息

不需要建立连接,就可以通信的。

特点:不占用连接、不可靠(消息可能因为网络不稳定而丢失)、快

四、tcp协议的代码

操作系统会统一分配计算机的所有资源

socket():tcp协议的server

socket(type=socket.SOCK_DGRAM):udp协议的server【udp协议中使用】

bind:绑定一个id和端口

listen:监听,代表socket服务的开启

accept:等到有客户端来访问,与客户端建立连接

send:直接通过连接发送消息,不需要写地址

sendto:需要写一个对方的地址【udp协议中使用】

recv:只接收消息

recvfrom:接收消息和地址【udp协议中使用】

connect:客户端/tcp协议的方法,和server端建立连接

close:关闭服务(sk.close)/连接(conn.close)

4.1 能够接收多个客户端的请求

server.py

import socket

sk = socket.socket()
sk.bind(('127.0.0.1', 9000))  # 申请操作系统的资源
sk.listen()

while True:
    # print(f'sk:{sk}')
    # conn里存储的是一个客户端和服务端的连接信息
    conn, adder = sk.accept()  # 能够和多个客户端进行握手了
    # print(f'conn:{conn}')
    conn.send(b'hello')
    msg = conn.recv(1024)
    print(msg)
    conn.close()  # 挥手,断开连接

sk.close()  # 归还申请的操作系统的资源

client.py

import socket

sk = socket.socket()

# 必须和server端的一致
sk.connect(('127.0.0.1', 9000))
msg = sk.recv(1024)
print(msg)

# 客户端 发送 消息给服务端
sk.send(b'bye')

sk.close()

4.2 在连接内,和客户端多说几句。以及解决不能传输中文问题

server.py

import socket

sk = socket.socket()
sk.bind(('127.0.0.1', 9000))  # 申请操作系统的资源
sk.listen()

while True:  # 为了和多个客户端进行握手
    # conn里存储的是一个客户端和服务端的连接信息
    conn, adder = sk.accept()  # 能够和多个客户端进行握手了

    while True:
        send_msg = input('please input you want say:')

        conn.send(send_msg.encode('utf-8'))
        if send_msg.upper() == 'Q':
            break
        msg = conn.recv(1024).decode('utf-8')
        if msg.upper() == 'Q':
            break
        print(msg)
    conn.close()  # 挥手,断开连接

sk.close()

client.py

import socket

sk = socket.socket()

sk.connect(('127.0.0.1', 9000))

while True:
    msg = sk.recv(1024).decode('utf-8')
    if msg.upper() == 'Q':
        break
    print(msg)

    send_msg = input('please input you want say:')
    sk.send(send_msg.encode('utf-8'))
    if send_msg.upper() == 'Q':
        break

sk.close()

4.3 程序执行到哪里会阻塞,什么时候结束阻塞

input() 等待,直到用户输入enter键

accept() 阻塞,有客户端来与我建立完连接之后

recv() 阻塞,直到收到对方发过来的消息之后

recvfrom() 阻塞,直到收到对方发过来的消息之后

connect() 阻塞,直到server端结束了对一个client的服务,开始和当前client建立连接的时候

五、udp协议的代码

server.py

import socket

sk = socket.socket(type=socket.SOCK_DGRAM)  # 表示一个udp协议
sk.bind(('127.0.0.1', 9000))

# 服务端不能先发送消息,因为服务端不知道客户端的ip
while True:
    msg, addr = sk.recvfrom(1024)
    print(msg.decode('utf-8'))
    send_msg = input('please input you want say:')
    sk.sendto(send_msg.encode('utf-8'), addr)
# server端不需要判断退出
# 因为不和这个客户端通信,还要和其他客户端通信

client.py

import socket

sk = socket.socket(type=socket.SOCK_DGRAM)
server = ('127.0.0.1', 9000)

while True:
    send_msg = input('please input you want say:')
    if send_msg.upper() == 'Q':
        break
    sk.sendto(send_msg.encode('utf-8'), server)
    msg = sk.recv(1024).decode('utf-8')
    if msg.upper() == 'Q':
        break
    print(msg)

六、粘包问题

6.1 tcp协议中的粘包现象

两条或更多条分开发送的信息连在一起就是粘包现象

server.py

import socket

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

conn, addr = sk.accept()
conn.send(b'hello')
conn.send(b'nice')
conn.close()
sk.close()


client.py

import time
import socket

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

time.sleep(0.1)
msg1 = sk.recv(1024)
print(msg1)
msg2 = sk.recv(1024)
print(msg2)

sk.close()

先启动server,然后启动client,client打印结果如下:

b'hellonice'
b''

产生了粘包现象,去掉睡眠时间结果如下:

b'hellonice'
b'nice'

【注意】即使不设置睡眠时间,也有几率粘包

6.2 产生粘包现象的两个原因

粘包现象只出现在tcp协议中,因为tcp协议中,多条消息之间没有边界(流式传输),并且还有一大堆优化算法。

  • 发送端:两条消息很短,并且发送的间隔时间也很短,由于优化机制就合并在一起发送了
  • 接收端:多条消息由于没有及时接收,在接收方的缓存中堆在一起而导致的粘包
  • 6.3 粘包发生的本质

    tcp协议的传输是流式传输,数据与数据之间没有边界

    6.4 解决粘包的问题的本质

    设置边界

    6.5 解决粘包

    自定义协议

    服务端:先发送4个字节的数据长度,再按照长度发送数据

    客户端:先接收4个字节,知道数据的长度,再按照长度接收数据

    6.6 struct模块

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

    数字的范围 -2**31-1 ~ 2**31,共 2**32 个数字。pack可以把这个范围内的任意一个数字转为长度为 4 的字节。

    import struct
    
    num1 = 123456789
    num2 = 12345
    num3 = 123
    
    ret1 = struct.pack('i', num1)
    print(ret1, len(ret1))  # b'\x15\xcd[\x07' 4
    ret2 = struct.pack('i', num2)
    print(ret2, len(ret2))  # b'90\x00\x00' 4
    ret3 = struct.pack('i', num3)
    print(ret3, len(ret3))  # b'{\x00\x00\x00' 4
    
    print(struct.unpack('i', ret1))  # (123456789,)
    print(struct.unpack('i', ret2))  # (12345,)
    print(struct.unpack('i', ret3))  # (123,)
    
    

    6.7 解决粘包代码

    server.py

    import socket
    import struct
    
    sk = socket.socket()
    sk.bind(('127.0.0.1', 9001))
    sk.listen()
    
    conn, addr = sk.accept()
    msg1 = input('>>>')
    msg2 = input('>>>')
    blen = struct.pack('i', len(msg1.encode()))
    conn.send(blen)
    conn.send(msg1.encode())
    conn.send(msg2.encode())
    conn.close()
    sk.close()
    
    

    client.py

    import time
    import socket
    import struct
    
    sk = socket.socket()
    sk.connect(('127.0.0.1', 9001))
    
    time.sleep(0.1)
    length = sk.recv(4)
    length = struct.unpack('i', length)[0]
    msg1 = sk.recv(length)
    msg2 = sk.recv(1024)
    
    print(msg1.decode('utf-8'))
    print(msg2.decode('utf-8'))
    
    sk.close()
    
    

    6.8 总结

    1. recv(1024)不代表一定收到1024个字节,而是最多只能收1024个字节
    2. 两条连续发送的数据一定要避免粘包问题
    3. tcp协议的自定义协议解决粘包问题的思路:先发送数据的长度,再发送数据。【将发送的数据组成json格式:先发送json的长度,再发json(json中存了接下来要发送的数据长度),再发数据】

    七、基于udp协议的多人聊天

    7.1 基于udp协议的多人聊天

    自动识别用户,不能使用 ip 和 port

    server.py

    import socket
    
    friend_lst = {'jack': '31', 'tom': '34'}
    sk = socket.socket(type=socket.SOCK_DGRAM)
    sk.bind(('127.0.0.1', 9000))
    while 1:
        msg, addr = sk.recvfrom(1024)
        msg = msg.decode('utf-8')
        name, message = msg.split('|')
        print(f'\033[1;{friend_lst.get(name,"30")}m {name}:{message} \033[0m')
        content = input('>>>')
        sk.sendto(content.encode('utf-8'), addr)
    
    

    client.py

    import socket
    
    name = 'jack'
    sk = socket.socket(type=socket.SOCK_DGRAM)
    
    while 1:
        content = input('>>>')
        if content.upper() == 'Q':
            break
        content = f'{name}|{content}'
        sk.sendto(content.encode('utf-8'), ('127.0.0.1', 9000))
        msg = sk.recv(1024).decode('utf-8')
        if msg.upper() == 'Q':
            break
        print(msg)
    
    

    八、基于tcp协议的文件传输

    server.py

    import socket
    import json
    # 接收
    sk = socket.socket()
    sk.bind(('127.0.0.1', 9002))
    sk.listen()
    conn, addr = sk.accept()
    msg = conn.recv(1024).decode('utf-8')
    msg = json.loads(msg)
    
    with open(msg['filename'], 'wb') as f:
        content = conn.recv(msg['filesize'])
        print('-->', len(content))
        f.write(content)
    
    print(msg)
    conn.close()
    sk.close()
    
    

    client.py

    import os
    import json
    import socket
    
    # 发送
    sk = socket.socket()
    sk.connect(('127.0.0.1', 9002))
    
    # 文件名、文件大小
    abs_path = r'E:\A.PythonProject\action\day30\作业\a.txt'
    filename = os.path.basename(abs_path)
    filesize = os.path.getsize(abs_path)
    dic = {'filename': filename, 'filesize': filesize}
    str_dic = json.dumps(dic)
    sk.send(str_dic.encode('utf-8'))
    
    with open(abs_path, 'rb') as f:
        content = f.read()
        sk.send(content)
    sk.close()
    
    

    8.1 优化

    上面代码存在的问题:发送的字典可能和文件内容发送粘包;发送大文件时,接收的文件大小比原来的小

    server.py

    import socket
    import json
    # 接收
    import struct
    
    sk = socket.socket()
    sk.bind(('127.0.0.1', 9002))
    sk.listen()
    conn, addr = sk.accept()
    msg_len = conn.recv(4)
    dic_len = struct.unpack('i', msg_len)[0]
    msg = conn.recv(dic_len).decode('utf-8')
    msg = json.loads(msg)
    
    with open(msg['filename'], 'wb') as f:
        while msg['filesize'] > 0:
            content = conn.recv(1024)
            # msg['filesize'] -= 1024 传过来的大小不一定是1024
            msg['filesize'] -= len(content)
            f.write(content)
    
    conn.close()
    sk.close()
    
    

    client.py

    import os
    import json
    import struct
    import socket
    
    # 发送
    sk = socket.socket()
    sk.connect(('127.0.0.1', 9002))
    
    # 文件名、文件大小
    # 必须是一个绝对路径
    abs_path = r'E:\A.PythonProject\action\day30\作业\a.txt'
    filename = os.path.basename(abs_path)
    filesize = os.path.getsize(abs_path)
    dic = {'filename': filename, 'filesize': filesize}
    str_dic = json.dumps(dic)
    b_dic = str_dic.encode('utf-8')
    mlen = struct.pack('i', len(b_dic))
    sk.send(mlen)  # 4个字节 表示字典转为字节之后的长度
    sk.send(b_dic)  # 具体的字典的数据
    
    with open(abs_path, 'rb') as f:
        while filesize > 0:
            content = f.read(1024)
            filesize -= 1024
            sk.send(content)
    sk.close()
    
    

    九、验证客户端的合法性

    server.py

    import hashlib
    import os
    import socket
    secret_key = b'nice@123!'
    sk = socket.socket()
    sk.bind(('127.0.0.1', 9001))
    sk.listen()
    
    conn, addr = sk.accept()
    # 创建一个随机的字符串
    rand = os.urandom(32)
    # 发送随机字符串
    conn.send(rand)
    # 根据发送的字符串 + secret_key 进行摘要
    sha = hashlib.sha1(secret_key)
    sha.update(rand)
    res = sha.hexdigest()
    # 等待接收客户端的摘要结果
    res_client = conn.recv(1024).decode('utf-8')
    # 做对比,如果一致,就显示是合法的客户端,并可以继续操作
    if res_client == res:
        print('是合法的客户端')
        conn.send(b'hello')
    else:
        # 如果不一致,应关闭连接
        conn.close()
    
    

    client.py

    import hashlib
    import socket
    secret_key = b'nice@123!'
    sk = socket.socket()
    sk.connect(('127.0.0.1', 9001))
    # 接收服务端发送的随机字符串
    rand = sk.recv(32)
    # 根据发送的字符串 + secret_key 进行摘要
    sha = hashlib.sha1(secret_key)
    sha.update(rand)
    res = sha.hexdigest()
    # 摘要结果发送回服务端
    sk.send(res.encode('utf-8'))
    # 继续和服务端进行通信
    msg = sk.recv(1024)
    print(msg)
    
    

    hmac模块

    用来替代 hashlib 模块

    import os
    import hmac
    
    h = hmac.new(b'nice@123!', os.urandom(32))
    ret = h.digest()
    print(ret)
    

    所以验证客户端合法性的代码可以简化。

    十、socketserver模块

    socketserver 模块是基于 socket 模块完成的,socket 模块是底层模块,封装度低,效率不固定;socketserver 模块封装度高,效率比较固定。

    10.1 作用

    tcp 协议的server端处理并发的客户端请求

    server.py

    import socketserver
    import time
    
    
    class Myserver(socketserver.BaseRequestHandler):
        # 重写handle
        def handle(self):
            # print(self.request)
            # <socket.socket fd=472, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 9001), raddr=('127.0.0.1', 9040)>
            # 相当于conn,既有自己的ip,又有客户端的ip
            conn = self.request
            while 1:
                try:
                    content = conn.recv(1024).decode('utf-8')
                    conn.send(content.upper().encode('utf-8'))
                    time.sleep(0.5)
                except ConnectionResetError:
                    break
    
    
    server = socketserver.ThreadingTCPServer(('127.0.0.1', 9001), Myserver)
    server.serve_forever()
    
    

    client.py

    import socket
    
    sk = socket.socket()
    sk.connect(('127.0.0.1', 9001))
    while 1:
        sk.send(b'hello')
        content = sk.recv(1024).decode('utf-8')
        print(content)
    
    

    10.2 注意

  • 只改变了server端的代码,client端代码没有改变
  • 当客户端来连接的时候直接和handle中的代码交互
  • 来源:lulu_aspirant

    物联沃分享整理
    物联沃-IOTWORD物联网 » Python网络编程(小白一看就懂)

    发表评论