初识python网络编程-01

近期学习python基础知识,到网络编程部分觉得很有意思,写个博客记录一下,首先可先了解一下计算机网络的基础知识,参考相关书籍即可,本文只做简单知识介绍!


1.OSI七层协议,TCP、UDP协议

首先, 我们今天使用的计算机都是要联网使用的. 很少有那种单机走天下的情况了. 那么我们的计算机是如何通过网络实现通信的. 我们先了解一些关于网络的基础知识. 然后再开始学习一些关于网络编程的内容, 第一个要解释的名词叫协议. 我们只有明白协议是什么, 后面再看各种各样的通信规则就容易的多了.
官话: 网络协议是通信计算机双方必须共同遵从的一组约定。如怎么样建立连接、怎么样互相识别等。只有遵守这个约定,计算机之间才能相互通信交流
普通话: 两台计算机之间约定好. 我发送的数据格式是什么. 你接收到数据之后. 使用相同的格式来拿到数据
例子: 你和一个韩国人交流. 你说中文, 他说韩文. 你俩是不能明白对方说的是什么的. 怎么办. 你俩约定好, 都说英语.实现交流. 这个就叫协议.
网络协议: 互联网之间互相传递消息的时候使用统一的一系列约定
在今天的互联网数据传输中一般使用的是OSI七层协议. 也有简称为五层, 四层协议. 只是对不同网络层的定义不同.
内部原理和作用是一样的.
初识python网络编程-01

下面简单介绍一下OSI七层协议的基本功能
首先是物理层: 该层是网络通信的数据传输介质,由连接不同结点的电缆与设备共同构成。主要跟功能是:利用传输介质为数据链路层提供物理连接,负责处理数据传输并监控数据出错率,以便数据流的透明传输
数据链路层: 这一层负责装配自己和对方主机的MAC地址. MAC地址: 每个网络设备的唯一编码. 全球唯一. 由不同厂商直接烧录在网卡上.
作用: 在庞大的网络系统中, 你要发送的数据到底是要给谁的. 由谁发送出来的. 这就相当于你写信的时候的信封. 上面得写清楚收货人地址.
网络层: 在有了MAC地址其实我们的电脑就可以开始通信了. 但是. 此时的通信方式是广播. 相当于通信基本靠吼. 你发送一个数据出去. 会自动的发给当前网络下的所有计算机. 然后每个计算机的网卡会看一眼这个数据是不是发给自己的. 像这样的通信方式, 如果计算机的数据量少. 是没有问题的. 但是. 如果全球所有计算机都按照这样的方式来传输消息. 那不仅是效率的问题了. 绝对是灾难性的. 那怎么办. 大家就想到了一个新的方案, 这个方案叫IP协议. 使用IP协议就把不同区域的计算机划分成一个一个的子网. 子网内的通信使用广播来传递消息. 广播外通过路由进行传递消息. 你可以理解为不同快递公司的分拨中心. 我给你邮寄一个快递. 先看一下是不是自己区域的. 是自⼰己区域直接挨家挨户找就OK了. 但是如果不是我这个区域的. 就通过分拨中心(路由器网关)找到你所在的区域的分拨中心(路由器网关), 再通过你的分拨中心下发给你. 这里IP协议的作用就体现出来了. 专门用来划分子网的.那么在传输数据的时候就必须要把对方的ip地址带着. 有了这个ip再加上子网掩码就可以判断出该数据到底是属于哪个子网下的数据.
IP地址: 由4位点分⼗十进制表示. 每位最⼤大255. 故IP地址的范围: 0.0.0.0~255.255.255.255. 为什什么是255, 答:
28 每一位用8位2进制表示, 合起来32位就可以表示一个计算机的ip地址
子网掩码: 用来划分子网的一个4位点分十进制.
网关: 路由器在子网内的ip. 不同局域网进行数据传输的接口(分拨中心)
计算子网的过程:

1 ip1: 192.168.123.16
2 ip2: 192.168.123.45

3 子网掩码: 255.255.255.0

4 全部转化成二进制
4 ip1: 11000000 10101000 01111011 00010000
5 ip2: 11000000 10101000 01111011 00101101
6 子网: 11111111 11111111 11111111 00000000

7 让ip1和ip2分别和子网进行"与"运算
8 ip1 & 子网: 11000000 10101000 01111011 00000000
9 ip2 & 子网: 11000000 10101000 01111011 00000000
10 相等. OK 这两个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编程
在几乎所有的编程语言中, 我们在编写网络程序的时候都要使用到socket. socket翻译过来叫套接字. 我们上面也了解到了一次网络通信的数据需要包裹着mac, ip, port等信息. 但是如果每次我们开发都要程序员去⼀个一个的去准备数据, 那工作量绝对是绝望的. 所以, 计算机提出了了socket. socket帮助我们完成了网络通信中的绝大多数操作. 我们只需要告诉socket. 我要向哪台计算机(ip, port)发送数据. 剩下的所有东西都由socket帮我们完成. 所以使用socket完成数据传输是非常方便的.
话不多说,练起来吧~~

server端:

import socket

#创建socket通道
sk = socket.socket()
sk.bind(("127.0.0.1", 8088))  # 绑定ip和端口
sk.listen()  # 开始监听
print("服务器就绪,等待连接")
conn, address = sk.accept()  # 程序阻塞,等待连接
print("有客户端连接,ip是:", address)  # address是客户端的ip和端口

while 1:   #能持续向客户端传递消息
    conn.send(input(">>>").encode("utf-8"))  # 发送的内容只能是bytes
    print(conn.recv(1024).decode("utf-8"))  #展示接收到的消息

client端:

import socket

sk = socket.socket()  # 创建通道
print("客户端初始化完成")
sk.connect(("127.0.0.1", 8088))  # 建立连接
print("客户端连接成功")
while 1:   #持续向服务端发送消息
    print(sk.recv(1024).decode("utf-8"))  # 最大接受1024字节的内容
    sk.send(input(">>>").encode("utf-8")) #展示接收到的消息

3.初识Socket-UDP编程
server端:

import socket

sk = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)
sk.bind(("127.0.0.1", 8089))
msg, address = sk.recvfrom(1024)
print(msg)
sk.sendto(b'hi', address)

client端:

import socket

sk = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)

sk.sendto(b'hello', ("127.0.0.1", 8089))
msg, address = sk.recvfrom(1024)
print(msg)

跟TCP编程原理差不多,只是UDP不用长时间建立连接,发送一个包,不用等回复,也不需要理会对方收到还是没收到,没有严格意义上的服务端和客户端


4.黏包现象
在使用TCP协议进行数据传输的时候, 会有以下问题出现.
client端:

import socket

sk = socket.socket()  # 创建通道
print("客户端初始化完成")
sk.connect(("127.0.0.1", 8088))  # 建立连接
print("客户端连接成功")

sk.send("哈哈".encode("utf-8"))
sk.send("哈哈".encode("utf-8")) #发送两次
print("发送完毕")
sk.close()

server端:

import socket

# 创建socket通道
sk = socket.socket()
sk.bind(("127.0.0.1", 8088))  # 绑定ip和端口
sk.listen()  # 开始监听
print("服务器就绪,等待连接")
conn, address = sk.accept()  # 程序阻塞,等待连接
print("有客户端连接,ip是:", address)  # address是客户端的ip和端口

msg1 = conn.recv(1024)
print(msg1.decode("utf-8"))
msg2 = conn.recv(1024)
print(msg2.decode("utf-8"))
sk.close()

server端收到的内容:
初识python网络编程-01
可以看到,两次发送的内容,黏在一起,变成了一个包,就是典型的黏包现象。
那么如何解决黏包问题呢? 很简单. 之所以出现黏包就是因为数据没有边界. 直接把两个包混合成了一个包. 那么我可以在发送数据的时候. 指定边界. 告诉对方. 我接下来这个数据包有多大. 对面接收数据的时候呢, 先读取该数据包的大小.然后再读取数据. 就不会产生黏包了.
普通话: 发送数据的时候制定数据的格式: 长度+数据 接收的时候就知道有多少是当前这个数据包的大小了. 也就相当于定义了分隔边界了.
展示一波操作:
client端:

import socket

sk = socket.socket()  # 创建通道
print("客户端初始化完成")
sk.connect(("127.0.0.1", 8088))  # 建立连接
print("客户端连接成功")

s = "哈哈"
bs = sk.send(s.encode("utf-8"))
# 计算数据长度.格式化成四位数字
bs_len = format(len(bs), "04d").encode("utf-8")
# 发送数据之前,先发送数据的长度
sk.send(bs_len)
sk.send(bs)

# 发送第二次
sk.send(bs_len)
sk.send(bs)

print("发送完毕")
sk.close()

server端:

import socket

# 创建socket通道
sk = socket.socket()
sk.bind(("127.0.0.1", 8088))  # 绑定ip和端口
sk.listen()  # 开始监听
print("服务器就绪,等待连接")
conn, address = sk.accept()  # 程序阻塞,等待连接
print("有客户端连接,ip是:", address)  # address是客户端的ip和端口

# 接收数据长度4个字节,转化成数字
bs_len = int(conn.recv(4).decode("utf-8"))
# 读取数据
msg1 = conn.recv(1024)
print(msg1.decode("utf-8"))
# 再接收数据长度,读取数据
bs_len = int(conn.recv(4).decode("utf-8"))
msg2 = conn.recv(1024)
print(msg2.decode("utf-8"))
sk.close()

但是,你应该发现了,这种传输的方式虽然能解决黏包问题,但是每次接收都要先定义接收的长度,喵的岂不是太累了??这个问题,python也有自己的态度,代码坚决要给你搞简单,就引入了一个新的模块struct,这是重点,拿小本本记下来,要考!!
展示一下struct的用法:

import struct

ret = struct.pack("i", 123456789)  # 把数字打包成字节
print(ret)

print(len(ret))  # 4  不论数字大小,定死了4个字节

# 把字节还原回数字
ds = b'\x15\xcd[\x07'
num = struct.unpack("i", ds)[0]   # num返回的是一个元祖,取索引为0的元素,也就是第一个元素,就是我们传输的数字
print(num)   # 123456789

好的,回归到黏包的问题上来,论如何优雅的解决黏包的问题:
client端:

import socket
import struct

sk = socket.socket()  # 创建通道
print("客户端初始化完成")
sk.connect(("127.0.0.1", 8088))  # 建立连接
print("客户端连接成功")

msg_bs = "你好呀".encode("utf-8")
msg_struct_len = struct.pack("i", len(msg_bs))
sk.send(msg_struct_len)
sk.send(msg_bs)
print("发送完毕")
# 发送第二次
sk.send(msg_struct_len)
sk.send(msg_bs)
print("发送完毕")

sk.close()

server端:

import socket
import struct

# 创建socket通道
sk = socket.socket()
sk.bind(("127.0.0.1", 8088))  # 绑定ip和端口
sk.listen()  # 开始监听
print("服务器就绪,等待连接")
conn, address = sk.accept()  # 程序阻塞,等待连接
print("有客户端连接,ip是:", address)  # address是客户端的ip和端口

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"))
print("接收完毕")
# 接收第二个包
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"))
print("接收完毕")

sk.close()

但是吧,这样也是稍显繁琐了,如果多次使用,也可将用到的struct模块,封装起来,需要用到的时候,就导入这个自定义的模块:
封装的模块my_struct_util.py:

import struct

def my_send(sk, msg):
    msg_len = msg.encode("utf-8")
    msg_struct_len = struct.pack("i", len(msg_len))
    sk.send(msg_struct_len)
    sk.send(msg_len)

def my_recv(sk):
    msg_struct_len = sk.recv(4)
    msg_len = struct.unpack("i", msg_struct_len)[0]
    data = sk.recv(msg_len)
    print(data.decode("utf-8"))

client端:

import socket
import my_socket_util as msu

sk = socket.socket()  # 创建通道
print("客户端初始化完成")
sk.connect(("127.0.0.1", 8088))  # 建立连接
print("客户端连接成功")
msu.my_send(sk, "你好吗")
msu.my_send(sk, "你好吗")

sk.close()

server端:

import socket
import my_socket_util as msu

# 创建socket通道
sk = socket.socket()
sk.bind(("127.0.0.1", 8088))  # 绑定ip和端口
sk.listen()  # 开始监听
print("服务器就绪,等待连接")
conn, address = sk.accept()  # 程序阻塞,等待连接
print("有客户端连接,ip是:", address)  # address是客户端的ip和端口

msu.my_recv(conn)
msu.my_recv(conn)
sk.close()

这样,是不是就简便了很多呢~~

猜你喜欢

转载自blog.51cto.com/14226596/2457271