打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
【连载电子书八】Python网络编程

网络基础

osi七层、tcp、udp协议

初识socket——tcp协议

初识socket——udp协议

黏包现象

优雅的解决黏包问题

文件上传

socketserver

1. osi七层, tcp, upd协议

首先, 我们今天使用的计算机都是要联网使用的. 很少有那种单机走天下的情况了.  那么我们的计算机是如何通过网络实现通信的. 我们先了解一些关于网络的基础知识. 然后再开始学习一些关于网络编程的内容, 第一个要解释的名词叫协议. 我们只有明白协议是什么, 后面再看各种各样的通信规则就容易的多了.

官话: 网络协议是通信计算机双方必须共同遵从的一组约定。如怎么样建立连接、怎么样互相识别等。只有遵守这个约定,计算机之间才能相互通信交流

普通话: 两台计算机之间约定好. 我发送的数据格式是什么. 你接收到数据之后. 使用相同的格式来拿到数据

例子:  你和一个韩国人交流. 你说中文, 他说韩文. 你俩是不能明白对方说的是什么的. 怎么办. 你俩约定好, 都说英语. 实现交流. 这个就叫协议.

网络协议: 互联网之间互相传递消息的时候使用统一的一系列约定

在今天的互联网数据传输中一般使用的是OSI七层协议. 也有简称为五层, 四层协议. 只是对不同网络层的定义不同. 内部原理和作用是一样的.

每一层分别是做什么的?  这里涉及到的网络的知识非常的复杂. 只做简单介绍. 如果你想深入研究. 可以参考大学课本(计算机网络). 买最薄的那本就够了.

首先是物理层. 这一层没啥说的. 作用就是把0101等电信号发送出去.  

数据链路层: 这一层负责装配自己和对方主机的MAC地址.  MAC地址: 每个网络设备的唯一编码. 全球唯一. 由不同厂商直接烧录在网卡上.

作用: 在庞大的网络系统中, 你要发送的数据到底是要给谁的. 由谁发送出来的. 这就相当于你写信的时候的信封. 上面得写清楚收货人地址.

网络层:  

在有了MAC地址其实我们的电脑就可以开始通信了. 但是. 此时的通信方式是广播.  相当于通通信基本靠吼. 你发送一个数据出去. 会自动的发给当前网络下的所有计算机. 然后每个计算机的网卡会看一眼这个数据是不是发给自己的. 像这样的通信方式, 如果计算机的数据量少. 是没有问题的. 但是. 如果全球所有计算机都按照这样的方式来传输消息. 那不仅仅是效率的问题了. 绝对是灾难性的. 那怎么办. 大家就想到了一个新的方案, 这个方案叫IP协议. 使用IP协议就把不同区域的计算机划分成一个一个的子网. 子网内的通信使用广播来传递消息. 广播外通过路由进行传递消息. 你可以理解为不同快递公司的分拨中心. 我给你邮寄一个快递. 先看一下是不是自己区域的. 是自己区域直接挨家挨户找就OK了. 但是如果你不是我这个区域的. 就通过分拨中心(路由器网关)找到你所在的区域的分拨中心(路由器网关), 再通过你的分拨中心下发给你. 这里IP协议的作用就体现出来了. 专门用来划分子网的.  

那么在传输数据的时候就必须要把对方的ip地址带着. 有了这个ip再加上子网掩码就可以判断出该数据到底是属于哪个子网下的数据.  

IP地址: 由4位点分十进制表示. 每位最大255. 故IP地址的范围: 0.0.0.0~255.255.255.255. 为什么是255, 答:  2**8 每一位用8位2进制表示, 合起来32位就可以表示一个计算机的ip地址

子网掩码: 用来划分子网的一个4位点分十进制.

网关: 路由器在子网内的ip. 不同局域网进行数据传输的接口(分拨中心)

计算子网的过程(了解):

ip1: 192.168.123.16
ip2: 192.168.123.45
子网掩码: 255.255.255.0
全部转化成二进制
ip1: 11000000 10101000 01111011 00010000
ip2: 11000000 10101000 01111011 00101101 
子网: 11111111 11111111 11111111 00000000
让ip1和ip2分别和子网进行'与'运算
ip1 & 子网: 11000000 10101000 01111011 00000000
ip2 & 子网: 11000000 10101000 01111011 00000000
相等. OK 这两个IP就是同一个子网

总结, 网络层提出了子网(局域网)的概念. 通过ip地址和子网掩码来划分子网

传输层:

我们现在解决了外界的数据传输问题. 使用MAC地址和IP地址可以唯一的定位到一台计算机了. 那么还有一个问题没有解决. 我们知道一台计算机内是很有可能运行着多个网络应用程序的. 比如, 你开着LOL, 挂着DNF, 聊着QQ, 还看着快播. 那么此时你的计算机网卡接收到了来自远方的一条数据.  那么这一条数据到底给那个应用呢? 说白了, 快递送到你公司了. 地址没毛病了. 可是你公司那么多人. 这个快递到底给谁? 不能乱给啊. 怎么办呢? 互联网大佬们想到了一个新词叫端口.

传输层规定: 给每一个应用程序分配一个唯一的端口号. 当有数据发送过来之后. 通过端口号来决定该数据发送的具体应用程序.

但是根据不同应用程序对网络的需求的不同(有的要求快, 有的要求可靠) 又把传输层划分成两个协议. 一个叫TCP, 一个叫UDP. 所以, 我们常说的TCP/IP协议中最重要, 也是我们最关注的其实就是IP和端口了. 因为有了这两个, 我们其实就可以定位到某一台计算机上的某个网络应用程序了. 也就可以给他发送消息了

32位计算机上的端口数:

TCP : 65536个

UDP: 65536个

TCP和UDP的区别:

1. TCP, 它是基于连接的. 是连续的, 可靠的. 效率比较低.  更像是打电话. 聊天的过程中不能中断.

2. UDP, 它不是基于连接的. 是不连续的, 不可靠的. 效率比较高.  更像是寄信, 今儿一封, 明儿一封. 想啥时候发什么时候发.

应用层:

这一层就比较好解释了. TCP+IP可以定位到计算机上的某个应用了. 但是不同应用传输的数据格式可能是不一样的. 就好比快递. 有的是大包裹. 有的是小文件. 一个要用大麻袋装, 一个要用小快递袋装.  到了应用层. 我们一般是根据不同类型的应用程序进行的再一次封装. 比如, HTTP协议, SMTP协议, FTP协议. 等等.

以上的网络基本概念, 我们就了解这么多. 如果你对网络内的知识想了解更多. 可以参考大学课本(计算机网络基础).

2. 初识Socket-TCP编程

在python, 哦不, 是几乎所有的编程语言中, 我们在编写网络程序的时候都要使用到socket. socket翻译过来叫套接字(很瘪嘴. 所以没人这么叫它). 我们上面也了解到了一次网络通信的数据需要包裹着mac, ip, port等信息. 但是如果每次我们开发都要程序员去一个一个的去准备数据, 那工作量绝对是绝望的. 所以, 计算机提出了socket. socket帮助我们完成了网络通信中的绝大多数操作. 我们只需要告诉socket. 我要向哪台计算机(ip, port)发送数据. 剩下的所有东西都由socket帮我们完成. 所以使用socket完成数据传输是非常方便的.

基本的socket-tcp编程

服务器:

import socket
# 创建一个socket通道
sk = socket.socket()    # 注册一家洗脚城
sk.bind(('127.0.0.1', 5000))  # 绑定一个ip和端口,  选址
sk.listen()  # 开始监听   开张
print('服务器端准备就绪, 等待连接')
conn, address = sk.accept()  # 程序会停在这里. 阻塞  等待客人上门
print('有人来连我了, 他的地址是:', address) # 这个address里放着你要的客户端ip和端口
conn.send('来啊, 大爷, 来玩儿啊'.encode('utf-8')) # 发送出去的内容只能是bytes. 给客人服务

客户端:

import socket
sk = socket.socket()        # 知道有洗脚城
print('客户端初始化完毕')
sk.connect(('127.0.0.1', 5000))  # 建立链接  登门拜访xxxx洗脚城
print('客户端链接成功')
print(sk.recv(1024).decode('utf-8')) # 最大接收1024个字节的内容 接收服务

此时, 我们可以让客户端和服务器端进行基本的网络通信了.  那如果是想要连续发送数据怎么办? 加循环啊

服务器:

import socket
sk = socket.socket()
sk.bind(('127.0.0.1', 5000))
sk.listen()
conn, address = sk.accept()
while 1:
conn.send(input('>>>:').encode('utf-8'))
    msg = conn.recv(1024).decode('utf-8')
print('收到的内容是: ', msg)

客户端:

import socket
sk = socket.socket()
sk.connect(('127.0.0.1', 5000))
while 1:
msg = sk.recv(1024).decode('utf-8')
    print('收到的内容是:', msg)
sk.send(input('>>>:').encode('utf-8'))

最基本的TCP编程咱们就先了解这么多. 稍后还会继续进行拓展的.

3. 初识Socket-UDP编程

使用udp编程

server端:

import socket
sk = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)
sk.bind(('127.0.0.1', 12123))
msg, address = sk.recvfrom(1024)
print(msg)
sk.sendto(b'i love you', address)

client端

import socket
sk = socket.socket(type=socket.SOCK_DGRAM)
sk.sendto(b'hello', ('127.0.0.1', 12123))
msg, addr = sk.recvfrom(1024)
print(msg)

4. TCP和UDP的对比

两种网络编程的方式最基本的代码已经OK了. 也能进行通信了. 那么问题来了. TCP和UDP的区别是什么?

TCP是基于链接的. 在传输数据之前必须保证链接的存在. 通信是通过conn来完成的. 所以, TCP通信具有以下特点:

1. 可靠

2. 连续

3. 效率低

三次握手

TCP协议, 为了保障数据传输的连续性可靠性. 建立链接的时候. 会给服务器发送一个x. 服务器返回客户端x+1同时返回一个y, 客户端接收到y, 返回y+1作为回执. 用来保证客户端和服务器是正常链接状态. 之后才开始进行传输数据.  有点儿类似我们打电话的时候,  

我: 你好, 是周杰伦么

你: 你好, 我是周杰伦, 你是王力宏么

我: 我是王力宏, 咱俩开始聊天吧

目的就是确保在传输数据之前我们两个身份确认成功.

四次挥手:

在传输完数据之后, 客户端会告诉服务器. 我这完事儿了. 给一个状态x+2,y+1.  服务器收到之后. 返回x+3, 等数据接收完毕之后返回y+1. 告诉客户端传输完毕了. 客户端最终返回y+2作为回执. 告诉服务器. 我走了.

我: 我要说的就这么多

你: 好的, 我记一下

你: 我记好了. 你可以滚蛋了

我: 好的,我滚了

经典问题: 为什么TCP协议是三次握手, 四次挥手?

因为在握手的时候, 两边其实还没有开始发送数据. 只是建立链接, 假设数据从Client向Server端发送. 客户端先发送一个SYN报文告诉服务器. 我要连你, 服务器回客户端, 我收到了(ACK), 相应的, 服务器也可以给客户端发送数据啊. 服务器也要发送一个SYN报文. 所以呢, 为了省事儿. 服务器把发送请求的SYN报文和应答的ACK报文就一起发出去了.  最后客户端发送一个应答(ACK) 这是三次握手. 实际上是4个动作. 只不过中间的服务器的SYN和ACK是一起发的.

那么在挥手的时候为什么是次呢. 梳理一下过程: 客户端发送完数据. 给服务器放FIN报文. 告诉服务器. 我完事儿了.  服务器先答应一下(ACK), oK 我知道了. 然后此时. 服务器有可能还有数据没发完呢. 只是你客户端说的. 你完事儿了. 我这不一定完事儿了啊. 所以, 服务器就只是发送了一个ACK, 告诉客户端, 我收到了. 等服务器这边忙完了. 该收的收了. 该发的发了. 服务器给客户端发送一个FIN报文. 告诉客户端, 我这也完事儿了.  然后客户端说: 好的(ACK) . 整个链接关闭.

我什么是4次挥手呢. 因为不论哪一方发起的FIN. 对方只能先回应一下(ACK). 得等对方完成了相关操作, 才能返回FIN.  最后在ACK. 所以. 关闭链接必须是4步.

5. 黏包现象

在使用TCP协议进行数据传输的时候, 会有以下问题出现.

client:

import socket
sk = socket.socket()
sk.connect(('127.0.0.1', 8101))
# 连续发送数据
s = '我爱你'
sk.send(s.encode('utf-8'))
sk.send(s.encode('utf-8'))
print('发送完毕')
sk.close()

server:

import socket
sk = socket.socket()
sk.bind(('127.0.0.1', 8101))
sk.listen()
conn, addr = sk.accept()
msg1 = conn.recv(1024)
print(msg1.decode('utf-8'))
msg2 = conn.recv(1024)
print(msg2.decode('utf-8'))
sk.close()

运行结果

我们发现, 打印来的效果是两个数据包合在一起了.  为什么会这样呢?  在数据传输的时候客户端发送两次数据. 这两个数据并不是直接发送出去的. 首先会存放在缓冲区. 等缓冲区数据装满或者经过一段时间后. 会把缓冲区中的数据一起发送出去.  这就导致了一个很坑的现象. 明明是两次发送的数据. 被合在了一起. 这就是典型的黏包现象.  

注意, 黏包现象只有TCP才会出现. UDP是不会出现黏包的. 因为UDP的不连续性. 每次发送的数据都会立刻打包成数据包然后发出去. 数据包与数据包之间是有边界隔离的. 你可以认为是一个sendto对应一个recvfrom. 因此UDP不会出现黏包.

那么如何解决黏包问题呢? 很简单. 之所以出现黏包就是因为数据没有边界. 直接把两个包混合成了一个包. 那么我可以在发送数据的时候. 指定边界. 告诉对方. 我接下来这个数据包有多大.  对面接收数据的时候呢, 先读取该数据包的大小.然后再读取数据. 就不会产生黏包了.

普通话:  发送数据的时候制定数据的格式:   长度+数据   接收的时候就知道有多少是当前这个数据包的大小了. 也就相当于定义了分隔边界了.

client:

import socket
sk = socket.socket()
sk.connect(('127.0.0.1', 8101))
# 连续发送数据
s = '我爱你'
bs = s.encode('utf-8')
# 计算数据长度. 格式化成4位数字
bs_len = format(len(bs), '04d').encode('utf-8')
# 发送数据之前. 先发送长度
# 整个数据包: 0009\x\x\x\x\x\x...
sk.send(bs_len)
sk.send(bs)
sk.send(bs_len)
sk.send(bs)
print('发送完毕')
sk.close()

server:

import socket
sk = socket.socket()
sk.bind(('127.0.0.1', 8101))
sk.listen()
conn, addr = sk.accept()
# 整个数据包: 0009\x\x\x\x\x\x...
# 接收4个字节. 转换成数字
bs_len = int(conn.recv(4).decode('Utf-8'))
# 读取数据
msg1 = conn.recv(bs_len)
print(msg1.decode('utf-8'))
bs_len = int(conn.recv(4).decode('Utf-8'))
msg2 = conn.recv(bs_len)
print(msg2.decode('utf-8'))
sk.close()

如果每次发送数据都要经过这么一次. 属实有点儿累.  没关系. python提供了一个很好用的模块来帮我们解决这个恶心的问题

import struct
# 打包. 把一个数字打包成字节
ret = struct.pack('i', 123456789)
print(ret)
print(len(ret)) # 4 不论数字大小, 定死了4个字节
# 把字节还原回数字
bs = b'\x15\xcd[\x07'
num = struct.unpack('i', bs)[0]
print(num)

6. 优雅的解决黏包问题

client:

import socket
import struct
sk = socket.socket()
sk.connect(('127.0.0.1', 8123))
msg_bs = '我爱你'.encode('utf-8')
msg_struct_len = struct.pack('i', len(msg_bs))
# 发一次
sk.send(msg_struct_len)
sk.send(msg_bs)
# 发两次
sk.send(msg_struct_len)
sk.send(msg_bs)

server:

import socket
import struct
sk = socket.socket()
sk.bind(('127.0.0.1', 8123))
sk.listen()
conn, addr = sk.accept()
# 接收一个数据包
msg_struct_len = conn.recv(4)
msg_len = struct.unpack('i', msg_struct_len)[0]
data = conn.recv(msg_len)
print(data.decode('utf-8'))
# 接收第二个数据包
msg_struct_len = conn.recv(4)
msg_len = struct.unpack('i', msg_struct_len)[0]
data = conn.recv(msg_len)
print(data.decode('utf-8'))

看着还是别扭. 提取一个模块试试看

my_socket_util

import struct
def my_send(sk, msg):
msg_bs = msg.encode('utf-8')
    msg_struct_len = struct.pack('i', len(msg_bs))
    sk.send(msg_struct_len)
sk.send(msg_bs)
def my_recv(sk):
# 接收一个数据包
    msg_struct_len = sk.recv(4)
msg_len = struct.unpack('i', msg_struct_len)[0]
    data = sk.recv(msg_len)
return data.decode('utf-8')

client:

import socket
import my_sk_util as msu
sk = socket.socket()
sk.connect(('127.0.0.1', 8123))
msu.my_send(sk, '我爱你')
msu.my_send(sk, '我爱你')

server:

import socket
import my_sk_util as msu
sk = socket.socket()
sk.bind(('127.0.0.1', 8123))
sk.listen()
conn, addr = sk.accept()
print(msu.my_recv(conn))
print(msu.my_recv(conn))

7. 文件上传

client:

import socket
import os
import struct
import json
sk = socket.socket()
sk.connect(('192.168.11.16', 8123))
# 要发送的文件
file_path = 'my_sk_util.py'
# 拿到文件大小和文件名字
file_size = os.path.getsize(file_path)
file_name = os.path.basename(file_path)
# 组装一个字典.
file_json = {'file_name': file_name, 'file_size': file_size}
# 转化成json字符串, 里面存着数据
file_json_str = json.dumps(file_json)
# 把json字符串发送出去. 防止黏包. 需要先发送数据大小
file_json_bs = file_json_str.encode('utf-8')
file_len_bs = struct.pack('i', len(file_json_bs))
sk.send(file_len_bs)
# 发送json数据
sk.send(file_json_bs)
# 发送文件数据
with open(file_path, mode='rb') as f:
while file_size > 0:
        bs = f.read(1024)  # 每次最多发送1024个字节
sk.send(bs)
        file_size -= len(bs)  # 发一次少一些字节
print('上传完毕')
sk.close()

server:

import socket
import struct
import json
sk = socket.socket()
sk.bind(('192.168.11.16', 8123))
sk.listen()
conn, address = sk.accept()
# 接收json长度, 放黏包
file_json_len_bs = conn.recv(4)
file_json_len = struct.unpack('i', file_json_len_bs)[0]
# 获取json字符串
file_json_str = conn.recv(file_json_len).decode('utf-8')
# 转化回字典
file_json = json.loads(file_json_str)
with open(f'上传/{file_json['file_name']}', mode='wb') as f:
    while file_json['file_size'] > 0:
bs = conn.recv(1024)
        file_json['file_size'] -= len(bs)
f.write(bs)
        print('one part')
print('上传完毕')
sk.close()

文件下载的逻辑和上传的逻辑是一样的.

9.  socketserver

由于TCP是连续的. 就我们目前的代码而言.  服务器端是无法处理多个人的请求的(同时).  就比如. 写一个这样的代码:

server:

import socket
import struct
import json
import subprocess
sk = socket.socket()
sk.bind(('127.0.0.1', 12233))
sk.listen()
conn, address = sk.accept()
while 1:
    shell_len_bs = conn.recv(4)
shell_len = struct.unpack('i', shell_len_bs)[0]
    shell = conn.recv(shell_len).decode('utf-8')
    # 用来执行shell脚本.
ret = subprocess.Popen(shell, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
d = {'msg': ret.stdout.read().decode('utf-8'), 'error': ret.stderr.read().decode('utf-8')}
    msg_json_bs = json.dumps(d).encode('utf-8')
msg_json_len_bs = struct.pack('i', len(msg_json_bs))
conn.send(msg_json_len_bs)
    conn.send(msg_json_bs)

client:

import socket
import struct
import json
import subprocess
sk = socket.socket()
sk.bind(('127.0.0.1', 12233))
sk.listen()
conn, address = sk.accept()
while 1:
shell_len_bs = conn.recv(4)
    shell_len = struct.unpack('i', shell_len_bs)[0]
shell = conn.recv(shell_len).decode('utf-8')
ret = subprocess.Popen(shell, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
d = {'msg': ret.stdout.read().decode('utf-8'), 'error': ret.stderr.read().decode('utf-8')}
    msg_json_bs = json.dumps(d).encode('utf-8')
msg_json_len_bs = struct.pack('i', len(msg_json_bs))
conn.send(msg_json_len_bs)
    conn.send(msg_json_bs)

此时我们发现只有一个人能连接到服务器.  因为在服务器端只有accept来接收客户端的连接.  并且接收客户端链接和接发数据是串行的. 如果我们需要一个能并行执行的server怎么办呢?  python中提供了socketserver来解决这个问题

牛B版本socketserver:

import socketserver
import subprocess
import struct
import json
class MyServer(socketserver.BaseRequestHandler):
    def handle(self):
conn = self.request
        while 1:
shell_len_bs = conn.recv(4)
            shell_len = struct.unpack('i', shell_len_bs)[0]
shell = conn.recv(shell_len).decode('utf-8')
ret = subprocess.Popen(shell, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
d = {'msg':ret.stdout.read().decode('utf-8'), 'error': ret.stderr.read().decode('utf-8')}
            msg_json_bs = json.dumps(d).encode('utf-8')
msg_json_len_bs = struct.pack('i', len(msg_json_bs))
conn.send(msg_json_len_bs)
            conn.send(msg_json_bs)
if __name__ == '__main__':
    sock = socketserver.ThreadingTCPServer(('127.0.0.1', 8991), MyServer)
sock.serve_forever()

作业题 :

1.  把上面的代码写一遍. 熟悉一下网络编程的相关代码. 以及黏包. 自定义头信息.

2.  完成一个FTP项目开发

需求:

1. 多用户同时登陆

2. 上传/下载文件

3. 传输过程中现实进度条

4. 不同用户使用自己的目录

5. 每个用户拥有1G的磁盘空间

6. 充分使用面向对象知识

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
linux 内核和用户空间通信之netlink使用方法
Python基于TCP与UDP协议实现Socket通信
Python网络编程 —— 粘包问题及解决方法
玩转Linux内核套接字(socket)原理与机制
网络编程(2)
几种TCP连接中出现RST的情况( 比较详细)
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服