网络编程

  • 基础概念
  • arp协议
  • TCP协议和UDP协议
  • TCP协议
  • UDP协议
  • 互联网的五层协议
  • Socket通信
  • TCP协议的Socket
  • UDP协议的Socket
  • TCP客户端执行服务端命令
  • 黏包
  • 黏包的解决
  • 黏包的解决进阶
  • 自定义报头
  • 实现一个视频的上传/下载


基础概念

C/S  客户端 ——服务器			应用类
B/S 	浏览器——服务器		web类
B/S架构中浏览器就是一个统一入口,通过浏览器访问各个网页
C/S中最典型的统一入口就是微信小程序

网络编程就是不同机器间的数据传输
对于电脑而言,要上网必须有网线,网线是由网卡支持(硬件上)
每一个网卡有一个全球唯一的mac地址
网卡的物理地址通常是由网卡生产厂家烧入网卡中
它存储的是传输数据时真正赖以标识发出数据的电脑和接收数据的主机的地址
在网络底层的物理传输过程中,是通过物理地址来识别主机的

IP协议:进行IP数据包的分割和组装
1.为每一台计算机分配IP地址
2.确定哪些地址在同一个子网络
ip地址:互联网上的一个逻辑地址,以此来屏蔽物理地址的差异。
常用的是ipv4——0.0.0.0 - 255.255.255.255
随着全球电脑用户增多,ip4不够用就出现了ipv6——0.0.0.0.0.0-255.255.255.255.255.255
ip地址相当于手机号,mac地址相当于手机出厂号
mac地址是固化在硬件中不可改变,ip地址可以人为改变

特殊的ip地址:127.0.0.1代表本地的回环地址,调用这个ip不会去找其他机器,不会经过网络
相当于cls表示自身类,self表示自身对象一样

arp协议

通过接受到的ip解析出对应的mac地址完成两台机器的通信

ARP协议的工作过程描述如下:
1、PC1希望将数据发往PC2,但它不知道PC2的MAC地址,因此发送了一个ARP请求,
该请求是一个广播包,向网络上的其它PC发出这样的询问:“192.168.0.2的MAC地址是什么?”,
网络上的其它PC都收到了这个广播包。

2、PC2看了这个广播包,发现其中的IP地址是我的,于是向PC1回复了一个数据包,
告诉PC1,我的MAC地址是00-aa-00-62-c6-09。PC3和PC4 收到广播包后,
发现其中的IP地址不是我的,因此保持沉默,不答复数据包。

3、PC1知道了PC2的MAC地址,它可以向PC2发送数据了。同时它更新了自己的ARP缓存表,
下次再向PC2发送信息时,直接从ARP缓存里查找PC2的MAC地址就可以了,
不需要再次发送ARP请求。

服务器发送数据包+ip到交换机上
交换机以广播的形式找ip
找到以以单播的形式将mac地址返回,这就是arp协议

广播风暴:当交换机找ip时会访问所有用户,当大量用户都在查找时,会占用大量的网络资源
出现广播风暴,严重的可以导致网络瘫痪
以广播方式发送的数据包只能局限于局域网内部
局域网中的主机要访问局域网以外的机器,需要经过网关


子网掩码:是一种用来指明一个IP地址的哪些位标识的是主机所在的子网,
以及哪些位标识的是主机的位掩码。子网掩码不能单独存在,它必须结合IP地址一起使用
子网掩码的二进制与ip的二进制数进行位与操作得到的数值转为十进制后的前三段即为局域网的网段
例:子网掩码 255.255.255.0
二进制:	1111 1111.1111 1111.1111 1111.0000 0000
	ip :	192.168.1.1
二进制:	1100 0000.1010 1000.0000 0001.0000 0001
相与		1100 0000.1010 1000.0000 0001.0000 0000
十进制:192.168.1.0  	网段即为192.168.1

电脑之间通过ip地址找到mac地址就可以找到对方了,但是电脑间的通信还需要一个程序,例如QQ等
不同电脑间程序如何找到对应的程序,这就需要端口号
端口号---具有网络功能的应用软件的标识号
开始---->运行---->cmd,或者是window+R组合键
netstat -ano,列出所有端口的情况,在这里中我们观察所有的端口
端口号是0到65535之间的一个整数
0到1023的端口被系统占用,用户只能选用大于1023的端口

TCP协议和UDP协议

TCP协议

在主机间建立一个虚拟连接,以实现高可靠性的数据包交换
	(ip协议与arp协议只能找对应的主机,不能保证通信的可靠性)
	一个应用程序通过TCP协议与另一个程序通信时,先进行三次握手后,
	在建立一个双方之间建立一个连接,所有数据就在这个连接中传递
	每一次收到信息都会进行反馈,表示这个信息我收到了,因此TCP是非常可靠的
	连接断开需要四次挥手
	常见面试题:握手要三次,挥手为什么要四次?

UDP协议

一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务
UDP提供了无连接通信,且不对传送数据包进行可靠性保证,
适合于一次传输少量数据,UDP传输的可靠性由应用层负责

二者区别是:

1、连接方面区别
TCP面向连接
UDP是无连接的,即发送数据之前不需要建立连接。
2、安全方面的区别
TCP提供可靠的服务,通过TCP连接传送的数据,无差错,不丢失,不重复
UDP不保证数据可靠性,可能会造成数据出错。
3、传输效率的区别
TCP要保证数据可靠,所以传输效率相对较低。
UDP不保证数据可靠,所以传输效率高
4、连接对象数量的区别
TCP连接只能是点到点、一对一的。
UDP支持一对一,一对多,多对一和多对多的交互通信。

互联网的五层协议

应用层:	python / java ....	收到"传输层"的数据,进行解读,规定应用程序的数据格式
传输层:	数据通信协议(TCP/UDP)	建立"端口到端口"的通信
网络层:	IP协议,引进一套网络地址,使得我们能够区分不同的计算机是否属于同一个子网络
数据链路层:		以太网协议 ,添加 mac地址 ,广播的发送方式,确定了0和1的分组方式
物理层: 它就是把电脑连接起来的物理手段,作用是负责传送0和1的电信号

网络地址帮助我们确定计算机所在的子网络,
MAC地址则将数据包送到该子网络中的目标网卡
因此,从逻辑上可以推断,必定是先处理网络地址,然后再处理MAC地址。
最后以太网的数据包就是 以太网的标头 + IP标头 + 通信协议标头 + 应用层数据包

Socket通信

socket(套接字)是应用层与TCP/IP协议簇通信的中间软件抽象层,它是一组接口

TCP/IP协议是一个协议簇。里面包括很多协议的。UDP只是其中的一个。
之所以命名为TCP/IP协议,因为TCP,IP协议是两个很重要的协议,就用他两命名了
TCP/IP协议集包括应用层,传输层,网络层,网络访问层。

TCP协议的Socket

服务端:创建socket ——绑定端口port——监听客户端请求——建立连接——收发数据——关闭socket
客户端:创建socket——建立连接——收发数据——关闭socket

server端

#基于TCP的socket
#server端
import socket
#创建socket
sock = socket.socket()
#绑定端口port
sock.bind(('127.0.0.1',8099))   #传入一个元组作为参数 ip地址 + 端口号
#监听客户端请求
sock.listen()
#建立连接
conn,addr = sock.accept() #得到一个连接对象和客户端地址
while True:
    ret = conn.recv(1024).decode('utf-8')   #接受客户数据,以1024字节读取
    print('server端:' + ret)
    if 'n' in ret:
        conn.send(b'n')
        break
    s = input('请输入...')
    conn.send(bytes(s,encoding = 'utf-8')) #发生数据,网络传输的数据一般为bytes类型
    
conn.close()    #关闭连接
print('server端——sock准备关闭')
#关闭socket
sock.close()

client端

#基于TCP的socket
#client端
import socket
#创建socket
sock = socket.socket()
sock.connect(('127.0.0.1',8099))   #发送请求到服务端
while True :
    s = input('请输入...')
    #发送数据
    sock.send(bytes(s,encoding = 'utf-8'))
    #接收数据
    ret = sock.recv(1024).decode('utf-8')
    print(ret)
    if 'n' in ret:
        sock.send(b'n')
        break
print('client端——sock准备关闭')
#关闭socket
sock.close()
tcp协议下的socket通信只能与单个主机通信

UDP协议的Socket

服务端:创建socket——绑定端口——收发信息——关闭socket
客户端:创建socket——收发信息——关闭socket

server端

#基于TCP的socket
#server端
import socket
#创建socket
sock = socket.socket(type=socket.SOCK_DGRAM)
#绑定端口port
sock.bind(('127.0.0.1',8099))   #传入一个元组作为参数 ip地址 + 端口号


while True:
    #接受数据
    byte,addr = sock.recvfrom(1024) #接受到客户端数据(bytes类型)和地址
    s = byte.decode('utf-8')
    print(s,addr)
    
    if 'n' in s:
        sock.sendto(b'n',addr)
        break
    s = input('请输入...')
    sock.sendto(bytes(s,encoding = 'utf-8'),addr) #发生数据,网络传输的数据一般为bytes类型
    

print('server端——sock准备关闭')
#关闭socket
sock.close()

client端`

#基于TCP的socket
#client端
import socket
#创建socket
sock = socket.socket(type=socket.SOCK_DGRAM)
#定义要请求的ip地址和端口号
iport = (('127.0.0.1',8099))
while True :
    s = input('请输入...')
    #发送数据
    sock.sendto(bytes(s,encoding = 'utf-8'),iport)
    #接收数据
    ret,addr = sock.recvfrom(1024)
    r = ret.decode('utf-8')
    print(r,addr)
    if 'n' in r:
        sock.sendto(b'n',iport)
        break
print('client端——sock准备关闭')
#关闭socket
sock.close()
TCP协议的socket通信中
需要通过监听客户端请求得到一个连接对象和客户端的ip+端口
通过这个连接对象来收发数据
连接对象.recv(b) 接受数据,类型为bytes类型
连接对象.send(s)	发送数据,需要的参数类型为bytes
最后连接对象需要关闭,sock对象也需要关闭
同一时间只能只能一对一

UDP协议的socket通信中
设置socket的类型socket.socket(type=socket.SOCK_DGRAM)
直接用socket对象收发数据,不保证数据的完整性
sock.recvfrom()	接收数据,类型为bytes
sock.sendto()	发送数据,参数1为bytes数据和参数2为发送数据的ip+端口

字典,元组,列表等数据需要序列化才能传输

TCP客户端执行服务端命令

服务端发送命令
客户端接收命令并执行

server端

import socket
#创建socket
sock = socket.socket()
#绑定端口port
sock.bind(('127.0.0.1',8099))  
#监听客户端请求
sock.listen()
#建立连接
conn,addr = sock.accept() #得到一个连接对象和客户端地址

cmd = input('请输入命令...') #输入要下达的指令,例如dir
conn.send(cmd.encode('utf-8'))  #发送指令
#接收反馈
ret = conn.recv(1024).decode('utf-8')
print(ret)	#输出指令返回的结果
conn.close()    #关闭连接
print('server端——sock准备关闭')
#关闭socket
sock.close()

client端`

#基于TCP的socket
#client端
import socket
import subprocess
#创建socket
sock = socket.socket()
sock.connect(('127.0.0.1',8099))   #发送请求到服务端
#接收指令
ret = sock.recv(1024).decode('utf-8')
r = subprocess.Popen(ret, shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
#参数1要执行的指令,参数2执行指令,
#参数3建立一个管道(类似容器,用于存放指令执行后返回的结果)
#参数4建立一个管道(存放错误信息)
r1 = r.stdout.read().decode('gbk')	#控制台一般都是gbk编码
r2 = r.stderr.read().decode('gbk')

out = 'stdout :' +  r1  #指令执行后的返回结果
err = 'stderr :' +  r2   #指令的错误信息

sock.send(out.encode('utf-8'))  #将结果返回给服务器
sock.send(err.encode('utf-8'))
    
print('client端——sock准备关闭')
#关闭socket
sock.close()
以上服务器接收到的客户端的数据结果不完整,只有一部分数据,这就是黏包现象
常常出现于两个send间
只有TCP有粘包现象,UDP永远不会粘包

黏包

tcp面向流的通信特点和Nagle算法使得数据不会丢失,同时相近时间内的小数据包会合并为
一个大数据包发送,而接收方recv(1024)按照指定的长度读取,造成只读取了一部分的数据,
剩余数据还在缓存中,等待下一次recv(1024),所以每一次接收的数据都是不完整的
黏包本质是因为接收方不知道按多长的指定字节长度来获取数据
即面向流的通信是无消息保护边界的
tcp面向流的通信没有数据包大小的限制(不考虑缓冲区的大小),数据过大时,会分段发送

udp是基于数据报的,输入的内容udp协议自动封装上消息头发送过去
采用链式结构记录每一个到达的数据包
这样的数据包可以根据消息头确定消息的边界了
即面向消息的通信是有消息保护边界的
但是接收数据有大小限制,最大为65500左右的字节
一次没有接收完的数据,剩下的数据会直接丢弃

黏包的解决

黏包本质是因为接收方不知道按多长的指定字节长度来获取数据
	解决:在send()接收数据之前,先获取本次要发送数据的总长度,
		将这个总长度send过去,用recv接收,此时对于一个数字而言占用的字节数很小,
		可以直接以1024代替本次recv()的长度,获取到了要发送过来的数据的总长度len
		再次设置recv的长度为recv(len)接收下次的数据就不会出现黏包现象了

client端`

r1 = r.stdout.read().decode('gbk')
r2 = r.stderr.read().decode('gbk')

out = 'stdout :' +  r1  #指令执行后的返回结果
err = 'stderr :' +  r2   #指令的错误信息

str_len = str(len(out) + len(err)) #计算数据总长度
print(str_len)
sock.send(str_len.encode('utf-8'))	#发送数据总长度
time.sleep(1)	#休眠1秒,返回时间过短和后续的send值合并为一个数据包
sock.send(out.encode('utf-8'))  #将结果返回给服务器
sock.send(err.encode('utf-8'))

server端

cmd = input('请输入命令...') #输入要下达的指令
conn.send(cmd.encode('utf-8'))  #发送指令
#接收反馈
ret = conn.recv(1024).decode('utf-8')   #接收到的是数据长度
int_len = int(ret)	#转换类型
rt = conn.recv(int_len).decode('utf-8')	#设置接收的长度
print(rt)

conn.close()    #关闭连接
print('server端——sock准备关闭')
#关闭socket
sock.close()
#send和sendto的数据过大,一样会出错
总结:文件传输中一定要明确接收方接收数据的大小
	 大文件传输一定要按照bytes类型传输(如视频,图片等),按固定字节传输
	 例如:1个52641大小的文件
	 发送方send(2048)   最后不足2048的  len % 2048
	 接收方recv(1024)	收发字节数可以不一致
	 或者使用内置的struct模块

黏包的解决进阶

struct模块

将非字符串类型的数据转为固定长度的bytes类型
	pack(参数类型,参数值) 	 将参数转为定长的bytes类型
	unpack(参数类型,参数值)	将定长的bytes类型转为原类型元素组成的元组
	int类型的固定长度是4个字节的bytes
import struct
ret = struct.pack('i', 1024)	#'i' 表示int类型
print(ret)  #b'\x00\x04\x00\x00'

num  = struct.unpack('i', ret)	#返回一个元组
print(num)  #(1024,)
利用struct模块我们可以将文件的总长度传递时,接收方recv(4)即可
发送文件长度后也不要time.sleep(1)防止黏包
就算数据都在一起发,接收方只接收recv(4)
保证了消息的边界

client端`

#基于TCP的socket
#client端
import socket
import subprocess
import struct
#创建socket
sock = socket.socket()
sock.connect(('127.0.0.1',8099))   #发送请求到服务端
#接收指令
ret = sock.recv(1024).decode('utf-8')
r = subprocess.Popen(ret, shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
#参数1要执行的指令,参数2执行指令,
#参数3建立一个管道(类似容器,用于存放指令执行后返回的结果)
#参数4建立一个管道(存放错误信息)
r1 = r.stdout.read().decode('gbk')#接收指令返回的结果
r2 = r.stderr.read().decode('gbk')#接收指令返回的错误信息

out = 'stdout :' +  r1  #指令执行后的返回结果拼接一个字符串标识
err = 'stderr :' +  r2   #指令的错误信息

int_len = len(out) + len(err)
print(int_len)	#3269 结果的总字节数
filesize = struct.pack('i',int_len )	#int转为定长的bytes数据
sock.send(filesize)

sock.send(out.encode('utf-8'))  #将结果返回给服务器
sock.send(err.encode('utf-8'))
    
print('client端——sock准备关闭')
#关闭socket
sock.close()

server端

import socket
import  struct
#创建socket
sock = socket.socket()
#绑定端口port
sock.bind(('127.0.0.1',8099))  
#监听客户端请求
sock.listen()
#建立连接
conn,addr = sock.accept() #得到一个连接对象和客户端地址

cmd = input('请输入命令...') #输入要下达的指令 例如dir
conn.send(cmd.encode('utf-8'))  #发送指令
#接收反馈
ret = conn.recv(4)   #接收到的是bytes类型的文件数据总长度
num = struct.unpack('i', ret)[0] #取出元组第一个元素

rt = conn.recv(num).decode('utf-8') #接收完整的文件数据
print(rt)

conn.close()    #关闭连接
print('server端——sock准备关闭')
#关闭socket
sock.close()

自定义报头

网络上传输的数据包中的报文通常包括报头+传输的实际内容
报头可以包括ip,mac地址,端口号等,可以根据需求自定义
1.用字典存储报头信息,比如传输的文件地址,文件名称,文件大小等待
2.获取报头大小,用struct模块将这个数值发送过去
3.将报头用json.dumps转为可传输的字符串再转为bytes传输
4.接收方接收到报头的大小后,以报头大小的字节数读取报头内容
5.将读取出报头用json.loads()转为字典
6.从字典中取出要传输文件的总大小
7.接收实际要传输的文件

实现一个视频的上传/下载

server端

接收上传的数据
import socket
import  struct
import configparser
import json
import time
config = configparser.ConfigParser()    #实例化一个configparser对象
config.read('example.ini')  #先读取配置文件
ip = config['Http']['ip']
port = int(config['Http']['port'])
by = int(config['Parameters']['server_bytes'])
#创建socket
sock = socket.socket()
#绑定端口port
sock.bind((ip,port))  
#监听客户端请求
sock.listen()
#建立连接
conn,addr = sock.accept() #得到一个连接对象和客户端地址


#接收反馈
ret = conn.recv(4)   #接收到报头的长度,类型为bytes
num = struct.unpack('i', ret)[0] #将bytes转为int类型的数值

rt = conn.recv(num).decode('utf-8') #接收报头内容并转为字符串
dic = json.loads(rt) #将字符串转为原先的字典
filesize =dic['filesize'] #取出要传输文件的大小

#保存文件
with open(dic['filename'],'wb') as f:
    time.sleep(3) #数据写的速度大于读的速度,所以停一会等文件读完上传后再写,否则数据会丢失一部分
    while filesize:
        print(filesize)
        if filesize >= by:
            content = conn.recv(by) #按指定字节读取,可以和传输的字节数不同
            f.write(content) #写入读取的内容
            filesize -= by  #剩余要写入的文件大小
        else:
            content = conn.recv(filesize)
            f.write(content)
            break
            
            
        
conn.close()    #关闭连接
print('server端——sock准备关闭')
#关闭socket
sock.close()

client端`

上传数据
#基于TCP的socket
#client端
import os
import json
import struct
import socket
import  configparser


con = configparser.ConfigParser()   #实例化一个configparser对象
con.read('example.ini') #读取配置参数
ip = con['Http']['ip']  #服务器ip
port = int(con['Http']['port'] ) #服务器端口
by = int(con['Parameters']['client_bytes'])  #按多少字节读取视频文件

#自定义报头
head = {
    'filepath':r'E:\BaiduNetdiskDownload',
    'filename':'1 捕捉异常.flv',
    'filesize':0
    }
file_path = os.path.join(head['filepath'],head['filename'])
print(file_path)
filesize = os.path.getsize(file_path)
print('文件大小为 :' ,end = ' ')
print(filesize)
head['filesize'] = filesize #存放要传输视频的大小

#将报头序列化,便于传输
json_str = json.dumps(head) #报头字典转为字符串

#字符串的字典转为bytes便于传输
json_bytes = json_str.encode('utf-8') #报头字符串转bytes

#获取要传输bytes字典的长度,用struct转为bytes传输
len_int = len(json_bytes)   #获取bytes的长度,类型为int

len_pack = struct.pack('i', len_int) #将int转为固定长度的bytes传输
#创建socket
sock = socket.socket()
sock.connect((ip,port))   #发送请求到服务端
sock.send(len_pack) #传递报头长度
sock.send(json_bytes) #传递报头内容

#传输文件
with open(file_path,'rb') as f:
    while filesize: #如果文件未读取完
        if filesize >= by: #剩余文件大小大于固定字节数
            content = f.read(by) #按固定字节读取指定字节
            sock.send(content)  #将读取的内容发送
            filesize -= by  #文件减去已经读取的大小
        else:
            content = f.read(filesize) #剩余大小不足一个标准字节数时直接读取完
            sock.send(content)
            break

 
print('client端——sock准备关闭')
#关闭socket
sock.close()