网络编程基础

一、互联网协议

  • 互联网协议是指国际互联网通信之间统一的标准,类似utf-8,国际通用语言英语
  • 网络就是物理链接介质+互联网协议
  • 按照功能不同,人们将互联网协议分为osi七层或tcp/ip五层或tcp/ip四层
    这种分层就好比是学习英语的几个阶段,每个阶段应该掌握专门的技能或者说完成特定的任务,比如:1、学音标 2、学单词 3、学语法 4、写作文
    在这里插入图片描述

二、TCP/IP五层模型

1.物理层

物理层功能:主要是基于电器特性发送高低电压(电信号),高电压对应数字1,低电压对应数字0
在这里插入图片描述

2.数据链路层

数据链路层由来:单纯的电信号0和1没有任何意义,必须规定电信号多少位一组,每组什么意思
数据链路层的功能:定义了电信号的分组方式

以太网协议

早期的时候各个公司都有自己的分组方式,后来形成了统一的标准,即以太网协议ethernet
ethernet规定

  • 一组电信号构成一个数据包,叫做‘帧’
  • 每一数据帧分成:报头head和数据data两部分
    head包含:(固定18个字节)
    发送者/源地址,6个字节
    接收者/目标地址,6个字节
  • 数据类型,6个字节
    data包含:(最短46字节,最长1500字节)
  • 数据包的具体内容
    head长度+data长度=最短64字节,最长1518字节,超过最大限制就分片发送
mac地址

head中包含的源和目标地址由来:ethernet规定接入internet的设备都必须具备网卡,发送端和接收端的地址便是指网卡的地址,即mac地址
mac地址:每块网卡出厂时都被烧制上一个世界唯一的mac地址,长度为48位2进制,通常由12位16进制数表示(前六位是厂商编号,后六位是流水线号)
在这里插入图片描述

广播

有了mac地址,同一网络内的两台主机就可以通信了(一台主机通过arp协议获取另外一台主机的mac地址)
ethernet采用最原始的方式,广播的方式进行通信,即计算机通信基本靠吼

3.网络层

在这里插入图片描述
网络层功能:引入一套新的地址用来区分不同的广播域/子网,这套地址即网络地址

IP协议
  • 规定网络地址的协议叫ip协议,它定义的地址称之为ip地址,广泛采用的v4版本即ipv4,它规定网络地址由32位2进制表示
  • 范围0.0.0.0-255.255.255.255
  • 一个ip地址通常写成四段十进制数,例:172.16.10.1
  • 回环地址(127.0.0.1) 又称为本机地址,平时我们用127.0.0.1来尝试自己的机器服务器好使不好使
4.传输层

传输层的由来:网络层的ip帮我们区分子网,以太网层的mac帮我们找到主机,然后大家使用的都是应用程序,你的电脑上可能同时开启qq,暴风影音,迅雷等多个应用程序,
那么我们通过ip和mac找到了一台特定的主机,如何标识这台主机上的应用程序呢?答案就是端口,端口即应用程序与网卡关联的编号。

传输层功能:建立端口到端口的通信
补充:端口范围0-65535,0-1023为系统占用端口
传输层有两种协议,TCP和UDP,见下图
在这里插入图片描述

TCP协议

可靠传输,TCP数据包没有长度限制,理论上可以无限长,但是为了保证网络的效率,通常TCP数据包的长度不会超过IP数据包的长度,以确保单个TCP数据包不必再分割。

  • 为什么tcp是可靠的数据传输呢?
    最可靠的方式就是只要不得到确认,就重新发送数据报,直到得到对方的确认为止。
  • tcp的3次握手和4四挥手:
    TCP协议建立C端和S端会通过3次握手建立双方之间的连接
    TCP协议断开C端和S端会通过4次回收断开双方之间的连接(因为C端发送断开请求时,S端数据并未确定传输结束,在答复C端FIN=1请求时ASK=1无法同时发出FIN=1请求)
    在这里插入图片描述
UDP协议

不可靠传输,”报头”部分一共只有8个字节,总长度不超过65,535字节,正好放进一个IP数据包。

  • UDP协议不同于TCP协议,在数据传输之前不需要建立链接,C端只负责将带有IP和端口信息的数据发出,不管对方是否接收到
  • 当遇到网络故障等问题时,UDP协议下的数据传输可能没有传到,但它并不会再次发出
对比

TCP协议虽然安全性很高,但是网络开销大,而UDP协议虽然没有提供安全机制,但是网络开销小,在现在这个网络安全已经相对较高的情况下,为了保证传输的速率,我们一般还是会优先考虑UDP协议!

三、SOCKET(套接字)

1.什么是SOCKET

由于TCP协议中的规则相当复杂,所以SOCKET把tcp/ip协议层的各种数据封装、数据发送、接收等通过代码已经封装好了,方便用户调用
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部。
在这里插入图片描述
SOCKET原理:
在这里插入图片描述

套接字家族分为两种:
  • AF_UNIX
    unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信
  • AF_INET
    AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET
2.简单的套接字通信

服务端代码

import socket

#1、买手机
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)	#socket实例化传入2个参数:套接字的家族名称,套接字传输数据方式(TCP协议用的是流水式)

#2、绑定手机卡
phone.bind(('127.0.0.1',8081)) #0-65535:0-1024给操作系统使用,IP和端口要以元组的形式传入

#3、开机
phone.listen(5) #5代表最大连接数

#4、等电话链接
print('starting...')
conn,client_addr=phone.accept() #phone.accept()接受到的数据是元组,conn代表链接(即电话线),client_addr代表客户端的地址

#5、收,发消息
data=conn.recv(1024) #1、单位:bytes 2、1024代表最大接收1024个bytes
print('客户端的数据',data)

conn.send(data.upper())

#6、挂电话
conn.close()

#7、关机
phone.close()

注意:在服务端中产生了2个套接字的对象:phone和conn,phone负责监听,conn负责收发消息
客户端代码:

import socket

#1、买手机
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)

#2、拨号
phone.connect(('127.0.0.1',8081))	#对应服务端的accept


#3、发,收消息
phone.send('hello'.encode('utf-8')) #底层物理层是进行二进制传输的,所以必须要发bytes类型
data=phone.recv(1024)
print(data)

#4、关闭
phone.close()

有的时候会遇到如下报错:
在这里插入图片描述
原因是因为上一次通信的端口还未回收,可以通过加入一条socket配置,重用ip和端口

phone=socket(AF_INET,SOCK_STREAM)
phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加
phone.bind(('127.0.0.1',8080))
3.加上通信循环

服务端代码

import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)    #生成一个电话对象
phone.bind(("127.0.0.1",1))     #绑定IP和端口
phone.listen(5)     #开始监听
conn,client_addr = phone.accept()   #收到请求,生成conn的收发对象
while True:
    data = conn.recv(1024)      #收到数据
    print(data)
    conn.send(data.upper())     #发出数据

conn.close()
phone.close()

客户端代码:

import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.connect(("127.0.0.1",1))

while True:
    msg = input(">>>").strip()
    phone.send(msg.encode("utf-8"))
    data = phone.recv(1024)
    print(data)

phone.close()

BUG解析:
服务端中conn是一个S端与C端双向链接的通道,如果中途C端终止会出现S端陷入死循环,Linux系统会不断地收到空数据,Window系统会直接报错ConnectionResetError,处理方案如下:

while True: #通信循环
    try:
        data=conn.recv(1024)
        if not data:break #适用于linux操作系统
        print('客户端的数据',data)

        conn.send(data.upper())
    except ConnectionResetError: #适用于windows操作系统
        break
4.实现服务端可以对多个客户端提供服务
import socket

phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
phone.bind(('127.0.0.1',8083)) #0-65535:0-1024给操作系统使用
phone.listen(5)

print('starting...')
while True: # 链接循环
    conn,client_addr=phone.accept()
    print(client_addr)

    while True: #通信循环
        try:
            data=conn.recv(1024)
            if not data:break #适用于linux操作系统
            print('客户端的数据',data)

            conn.send(data.upper())
        except ConnectionResetError: #适用于windows操作系统
            break
    conn.close()

phone.close()

链接循环——在创建链接conn前加上循环
这样做实现了服务端一个个的与客户端通信的功能,但是不能并发进行

4.模拟SSH远程执行命令

服务端代码:

import socket
import subprocess
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)    #生成一个电话对象
phone.bind(("127.0.0.1",1))     #绑定IP和端口
phone.listen(3)     #开始监听
while True:
    print("starting...")
    conn,client_addr = phone.accept()   #收到请求,生成conn的收发对象
    while True:
        cmd = conn.recv(1024)      #收到数据
        print(cmd)
        if not cmd:break
        obj = subprocess.Popen(cmd.decode("gbk"),shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
        # subhprocess.Popen生成了一个对象,shell=True代表打开一个命令解释器(cmd)
        # stdout代表命令执行成功将数据存入subprocess.PIPE里
        # stderr代表命令执行失败将数据存入subprocess.PIPE里
        stdout = obj.stdout.read()	#将数据读出来
        stderr = obj.stderr.read()
        conn.send(stdout+stderr)     #发出的数据为bytes格式
    conn.close()

phone.close()

客户端代码:

import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.connect(("127.0.0.1",1))

while True:
    cmd = input(">>>").strip()
    if not cmd:break
    phone.send(cmd.encode("utf-8"))

    data = phone.recv(1024)
    print(data.decode("gbk"))

phone.close()

四、粘包现象

1.什么是粘包?

粘包现象——在套接字中resv(1024)中1024代表一次数据接收1024个bytes的长度,那么当send数据超过1024时,例如1025字节数据中的最后一个数据会暂存在操作系统的内存中,待下一次resv数据一并传入
在这里插入图片描述
从五层协议看,应用程序只负责应用层,数据交给操作系统后由操作系统代为执行后续几层功能:

  1. 不管是recv还是send都不是直接接收对方的数据,而是操作自己的操作系统内存
    所以,并不是一次send就对应一次recv,可以send多次数据,只用一次resv接收
  2. resv接收数据分为两步:
    wait data等待接收数据,耗时较长
    copy data接收到数据后将数据从系统内存copy到应用程序内存中,速度较快
  3. send发送数据只有一步:
    copy data将数据复制给操作系统内存,等候操作系统将数据按照TCP协议发出即可
2.粘包的底层原理
Nagle算法

按照TCP协议,如果send数据内容较小,时间间隔较短,每次传输会发生网络I/O延迟(input/output),这样是非常不高效的
Nagle算法是将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包传输,那么到了接收端就会发生两次传输的数据包接连起来的情况
示例代码:
服务端代码:

import socket

server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.bind(("127.0.0.1",10086))
server.listen(5)
conn,client_addr = server.accept()

res = conn.recv(1024)
print(res.decode("utf-8"))

客户端代码:

import socket

client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(("127.0.0.1",10086))

client.send("hello".encode("utf-8"))
client.send("world".encode("utf-8"))

服务端输出的结果是:helloworld,会将客户端两次发送的数据"hello"和"world"粘合起来
但是当给客户端两次发送之间加入time.sleep(1)延迟一秒的时候,两次数据就不会粘合,因为1秒对于I/O延迟来说算很长了

3.Struct模块介绍

struct 模块是一个可以将任意大小的数字转换成一个固定长度编码的模块

struct.pack(“i”,data)
import struct

res = struct.pack("i",123)
print(res,type(res),len(res))
输出结果:
b'x\x00\x00\x00' <class 'bytes'> 4

可见struct.pack(“i”,123)得到的结果是一个bytes类型,并且长度固定为4个字节
但是这个转化对数字的大小范围有一定的要求
i 模式转换的数字较小,转化之后的结果只有4个字节
q 模式转换的数字范围较大,转换之后的结果有8个字节

struct.unpack(“i”,data)
import struct

res = struct.pack("i",120)
obj = struct.unpack("i",res)
print(obj)
输出结果:
(120,)

通过struct.unpack(“i”,data)反解可以得到pack打包的数据,它是一个元组形式

4.解决粘包问题(简单版)

思路解析——本质上是在发送数据之前,将数据的长度做一个固定长度的报头发送对方,对方在resv(***)的时候可以推算需要循环几次收完数据
服务端代码:

import socket
import struct

server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.bind(("127.0.0.1",10086))
server.listen(5)
conn,client_addr = server.accept()

#第一步:先收报头(肯定为4个字节长度)
header = conn.recv(4)

#第二步:从报头中解析出对真实数据的描述信息(数据的长度)
data_len = struct.unpack("i",header)[0]     #注意unpack拿到的数据是一个元组形式

#第三部:接收真实数据
resv_len = 0
resv_data = b""
while resv_len < data_len :
    res = conn.recv(1024)
    resv_len += len(res)    #收到数据的长度增加
    resv_data += res    #收到新的数据与老的数据相连
print(resv_data.decode("utf-8"))

客户端代码:

import socket
import struct

client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(("127.0.0.1",10086))

#第一步:制作固定长度的报头
data = input(">>>").strip().encode("utf-8")
data_len = len(data)
header = struct.pack("i",data_len)

#第二步:把报头发给服务端
client.send(header)

#第三步:发送真实数据
client.send(data)
5.解决粘包问题(终极版)

上面简单版的解决粘包问题存在两个缺陷:

  • 报头内容只有数据长度,有时我们还需要文件名,md5密码等信息
  • struct模块的i格式对于数字长度有要求,文件长度过长无法转换成4字节的报头
    基于以上做以下优化:
    思路——利用字典将文件名等信息加入报头,利用json将字典改为字符串格式,再转换bytes类型取得报头的长度,先传输报头,反解后获得字典中文件大小的key,再进行真实文件的传输
    服务端代码:
import socket
import struct
import json

server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.bind(("127.0.0.1",10086))
server.listen(5)
conn,client_addr = server.accept()

#第一步:先收报头的长度(肯定为4个字节长度)
obj = conn.recv(4)
header_len = struct.unpack("i",obj)[0]

#第二步:收报头
header_bytes = conn.recv(header_len)

#第三步:从报头中解析出对真实数据的描述信息(数据的长度)
header_json = header_bytes.decode("utf-8")
header_dict = json.loads(header_json)
data_len = header_dict["data_len"]

#第四部:接收真实数据
resv_len = 0
resv_data = b""
while resv_len < data_len :
    res = conn.recv(1024)
    resv_len += len(res)    #收到数据的长度增加
    resv_data += res    #收到新的数据与老的数据相连
print(resv_data.decode("utf-8"))

客户端代码:

import socket
import struct
import json

client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(("127.0.0.1",10086))

#第一步:制作固定长度的报头
data = input(">>>").strip().encode("utf-8")
data_len = len(data)
header_dict = {"filename":"Kerwin.txt","md5":"xxxxdxxx","data_len":data_len}
header_json = json.dumps(header_dict)
header_bytes = header_json.encode("utf-8")
header_len = struct.pack("i",len(header_bytes))

#第二步:把报头数据的长度发给服务端
client.send(header_len)

#第三步:把报头发给服务端
client.send(header_bytes)

#第四步:发送真实数据
client.send(data)

五、文件传输

1.基本功能实现

以下代码实现基础是将服务端与客户端创建在两个单独文件夹内,为避免两端同时进行wb和rb模式报错,实际最好加入filename的不同路径(共享文件夹和下载文件夹)
在这里插入图片描述
服务端代码:

import socket
import struct
import json

server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.bind(("127.0.0.1",10086))
server.listen(5)
conn,client_addr = server.accept()

#第一步:先收报头的长度(肯定为4个字节长度)
obj = conn.recv(4)
header_len = struct.unpack("i",obj)[0]

#第二步:收报头
header_bytes = conn.recv(header_len)

#第三步:从报头中解析出对真实数据的描述信息(数据的长度)
header_json = header_bytes.decode("utf-8")
header_dict = json.loads(header_json)
data_len = header_dict["data_len"]
filename = header_dict["filename"]

#第四部:通过wb的模式不断从客户端收到数据写入文件内
with open(filename,"wb") as f:
    resv_len = 0
    while resv_len < data_len :
        line = conn.recv(1024)
        f.write(line)    #收到新的数据与老的数据相连
        resv_len += len(line)    #收到数据的长度增加
        print("总大小:%s 已下载:%s"%(data_len,resv_len))  #打印数据下载进度条

客户端代码:

import socket
import struct
import json
import os

client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(("127.0.0.1",10086))

#第一步:收到命令
cmd = input(">>>").strip()  #get 1.mp4

#第二步:解析命令,获取相应命令信息
filename = cmd.split(" ")[1]    #文件名:1.mp4

#第三步:制作固定长度的报头
data_len = os.path.getsize(filename)    #通过os模块获取文件大小
header_dict = {"filename":filename,"md5":"xxxxdxxx","data_len":data_len}
header_json = json.dumps(header_dict)
header_bytes = header_json.encode("utf-8")
header_len = struct.pack("i",len(header_bytes))

#第四步:把报头数据的长度发给服务端
client.send(header_len)

#第五步:把报头发给服务端
client.send(header_bytes)

#第六步:通过rb打开文件,每行读取内容发送给服务端
with open(filename,"rb") as f:
    for line in f :
        client.send(line)

六、基于UDP协议的套接字(数据报)

1.代码示例

客户端代码:

import socket

client = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)	#SOCK_DGRAM是data gram缩写(数据报),不同于TCP协议的数据流
client.sendto(b"hello",("127.0.0.1",10086))

服务端代码:

import socket

server = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
server.bind(("127.0.0.1",10086))
while True:
    data = server.recvfrom(1024)
    print(data)
输出结果:
(b'hello', ('127.0.0.1', 65370))

特点:

  • UDP协议套接字不需要conn.listen,conn.accept等监听和接收链接的操作,因为它不存在双向链接
  • UDP协议的接收命令是recvfrom,发送命令是sendto
  • UDP协议的发送方只负责发送数据,一旦发送就会从内存中删除,当出现网络问题时,服务端没收齐数据时就会发送数据丢失,所以为不可靠的方式
  • 当recvfrom的字节量小于sendto的数据量时,windows系统会报错:
    在这里插入图片描述
2.TCP协议与UDP协议对比:
tcp基于链接通信
  • 基于链接,则需要listen(backlog),指定连接池的大小
  • 基于链接,必须先运行的服务端,然后客户端发起链接请求
  • 对于mac系统:如果一端断开了链接,那另外一端的链接也跟着完蛋recv将不会阻塞,收到的是空(解决方法是:服务端在收消息后加上if判断,空消息就break掉通信循环)
  • 对于windows/linux系统:如果一端断开了链接,那另外一端的链接也跟着完蛋recv将不会阻塞,收到的是空(解决方法是:服务端通信循环内加异常处理,捕捉到异常后就break掉通讯循环)
udp无链接
  • 无链接,因而无需listen(backlog),更加没有什么连接池之说了
  • 无链接,udp的sendinto不用管是否有一个正在运行的服务端,可以己端一个劲的发消息,只不过数据丢失
  • recvfrom收的数据小于sendinto发送的数据时,在mac和linux系统上数据直接丢失,在windows系统上发送的比接收的大直接报错
  • 只有sendinto发送数据没有recvfrom收数据,数据丢失
总结

TCP协议虽然安全性很高,但是网络开销大,而UDP协议虽然没有提供安全机制,但是网络开销小,在现在这个网络安全已经相对较高的情况下,为了保证传输的速率,我们一般还是会优先考虑UDP协议!
TCP协议常用情景:远程操控、文件传输
UDP协议常用情景:QQ聊天,查询信息

七、目录结构规范

(1)bin,bin下面主要是存放程序的执行入口。一般看见bin,启动程序都会在这个下面
(2)core,core下面主要是存放程序中使用的逻辑,核心代码,可以用包的来构成,这个包下面存放不同的功能模块,比如登陆认证,数据解析等程序的核心代码都是写到这个core的下面
(3)conf,程序的配置文件,程序中使用到的一些固定的变量,譬如listen监听的ip,端口这些都是固定的,就可以直接写到配置文件中,然后在程序中就是使用变量的引入,这样的好处就是以后修改的时候只需要修改配置文件,比如链接数据库的信息,全部都是放到这个文件加下
(4)doc,这个文件夹下存放文件的一些文件信息
(5)log,程序的日志信息
(6)db,db文件专门用来存放程序中使用的一切数据来源
(7)readme,程序的使用介绍,以及程序的一些说明等

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值