网络编程

一、软件开发架构

我们了解的涉及到两个程序之间通讯的应用大致可以分为两种:

第一种是应用类:qq、微信、网盘、优酷这一类是属于需要安装的桌面应用

第二种是web类:比如百度、知乎、博客园等使用浏览器访问就可以直接使用的应用

1.C/S架构

C/S即:Client与Server ,中文意思:客户端与服务器端架构,这种架构也是从用户层面(也可以是物理层面)来划分的。

这里的客户端一般泛指客户端应用程序EXE,程序需要先安装后,才能运行在用户的电脑上,对用户的电脑操作系统环境依赖较大。

2.B/S架构

B/S即:Browser与Server,中文意思:浏览器端与服务器端架构,这种架构是从用户层面来划分的。

Browser浏览器,其实也是一种Client客户端,只是这个客户端不需要大家去安装什么应用程序,只需在浏览器上通过HTTP请求服务器端相关的资源(网页资源),客户端Browser浏览器就能进行增删改查。

二、osi七层协议

引子

须知一个完整的计算机系统是由硬件、操作系统、应用软件三者组成,具备了这三个条件,一台计算机系统就可以自己跟自己玩了(打个单机游戏,玩个扫雷啥的)

如果你要跟别人一起玩,那你就需要上网了,什么是互联网?

互联网的核心就是由一堆协议组成,协议就是标准,比如全世界人通信的标准是英语,如果把计算机比作人,互联网协议就是计算机界的英语。所有的计算机都学会了互联网协议,那所有的计算机都就可以按照统一的标准去收发信息从而完成通信了。

osi七层模型

人们按照分工不同把互联网协议从逻辑上划分了层级:

tcp/ip五层模型讲解

首先,用户感知到的只是最上面一层应用层,自上而下每层都依赖于下一层,所以我们从最下一层开始切入,比较好理解

每层都运行特定的协议,越往上越靠近用户,越往下越靠近硬件

  1 
  2 
  3 物理层由来:上面提到,孤立的计算机之间要想一起玩,就必须接入internet,言外之意就是计算机之间必须完成组网
  4 
  5 物理层功能:主要是基于电器特性发送高低电压(电信号),高电压对应数字1,低电压对应数字0
  6 
物理连接层
  1 数据链路层由来:单纯的电信号0和1没有任何意义,必须规定电信号多少位一组,每组什么意思
  2 数据链路层的功能:定义了电信号的分组方式
  3 
  4 以太网协议:
  5 
  6 早期的时候各个公司都有自己的分组方式,后来形成了统一的标准,即以太网协议ethernet
  7 
  8 ethernet规定
  9 
 10 一组电信号构成一个数据包,叫做‘帧’
 11 每一数据帧分成:报头head和数据data两部分
 12 
 13 head包含:(固定18个字节)
 14 
 15 发送者/源地址,6个字节
 16 接收者/目标地址,6个字节
 17 数据类型,6个字节
 18 data包含:(最短46字节,最长1500字节)
 19 
 20 数据包的具体内容
 21 head长度+data长度=最短64字节,最长1518字节,超过最大限制就分片发送
 22 
 23 mac地址:
 24 
 25 head中包含的源和目标地址由来:ethernet规定接入internet的设备都必须具备网卡,发送端和接收端的地址便是指网卡的地址,即mac地址
 26 
 27 mac地址:每块网卡出厂时都被烧制上一个世界唯一的mac地址,长度为48位2进制,通常由12位16进制数表示(前六位是厂商编号,后六位是流水线号)
数据链路层
广播:

有了mac地址,同一网络内的两台主机就可以通信了(一台主机通过arp协议获取另外一台主机的mac地址)

ethernet采用最原始的方式,广播的方式进行通信,即计算机通信基本靠吼

  1 网络层由来:有了ethernet、mac地址、广播的发送方式,世界上的计算机就可以彼此通信了,问题是世界范围的互联网是由一个个彼此隔离的小的局域网组成的,那么如果所有的通信都采用以太网的广播方式,那么一台机器发送的包全世界都会收到,这就不仅仅是效率低的问题了,这会是一种灾难。
  2 
  3 网络层功能:引入一套新的地址用来区分不同的广播域/子网,这套地址即网络地址
  4 
  5 IP协议:
  6 
  7 规定网络地址的协议叫ip协议,它定义的地址称之为ip地址,广泛采用的v4版本即ipv4,它规定网络地址由32位2进制表示
  8 范围0.0.0.0-255.255.255.255
  9 一个ip地址通常写成四段十进制数,例:172.16.10.1
 10 ip地址分成两部分
 11 
 12 网络部分:标识子网
 13 主机部分:标识主机
 14 注意:单纯的ip地址段只是标识了ip地址的种类,从网络部分或主机部分都无法辨识一个ip所处的子网
 15 
 16 例:172.16.10.1与172.16.10.2并不能确定二者处于同一子网
 17 
 18 子网掩码
 19 
 20 所谓”子网掩码”,就是表示子网络特征的一个参数。它在形式上等同于IP地址,也是一个32位二进制数字,它的网络部分全部为1,主机部分全部为0。比如,IP地址172.16.10.1,如果已知网络部分是前24位,主机部分是后8位,那么子网络掩码就是11111111.11111111.11111111.00000000,写成十进制就是255.255.255.0。
 21 
 22 
 23 
 24 知道”子网掩码”,我们就能判断,任意两个IP地址是否处在同一个子网络。方法是将两个IP地址与子网掩码分别进行AND运算(两个数位都为1,运算结果为1,否则为0),然后比较结果是否相同,如果是的话,就表明它们在同一个子网络中,否则就不是。
 25 
 26 
 27 
 28 比如,已知IP地址172.16.10.1和172.16.10.2的子网掩码都是255.255.255.0,请问它们是否在同一个子网络?两者与子网掩码分别进行AND运算,
 29 
 30 172.16.10.1:10101100.00010000.00001010.000000001
 31 
 32 255255.255.255.0:11111111.11111111.11111111.00000000
 33 
 34 AND运算得网络地址结果:10101100.00010000.00001010.000000001->172.16.10.0
 35 
 36 
 37 
 38 172.16.10.2:10101100.00010000.00001010.000000010
 39 
 40 255255.255.255.0:11111111.11111111.11111111.00000000
 41 
 42 AND运算得网络地址结果:10101100.00010000.00001010.000000001->172.16.10.0
 43 
 44 结果都是172.16.10.0,因此它们在同一个子网络。
 45 
 46 
 47 总结一下,IP协议的作用主要有两个,一个是为每一台计算机分配IP地址,另一个是确定哪些地址在同一个子网络。
 48 
网络层
  1 arp协议由来:计算机通信基本靠吼,即广播的方式,所有上层的包到最后都要封装上以太网头,然后通过以太网协议发送,在谈及以太网协议时候,我门了解到通信是基于mac的广播方式实现,计算机在发包时,获取自身的mac是容易的,如何获取目标主机的mac,就需要通过arp协议
  2 
  3 arp协议功能:广播的方式发送数据包,获取目标主机的mac地址
  4 
  5 协议工作方式:每台主机ip都是已知的
  6 
  7 例如:主机172.16.10.10/24访问172.16.10.11/24
  8 
  9 一:首先通过ip地址和子网掩码区分出自己所处的子网
 10 
 11 场景
 12 数据包地址
 13 
 14 同一子网
 15 目标主机mac,目标主机ip
 16 
 17 不同子网
 18 网关mac,目标主机ip
 19 
 20 二:分析172.16.10.10/24与172.16.10.11/24处于同一网络(如果不是同一网络,那么下表中目标ip为172.16.10.1,通过arp获取的是网关的mac)
 21 
 22 源mac
 23 目标mac
 24 源ip
 25 目标ip
 26 数据部分
 27 
 28 发送端主机
 29 发送端mac
 30 FF:FF:FF:FF:FF:FF
 31 172.16.10.10/24
 32 172.16.10.11/24
 33 数据
 34 
 35 三:这个包会以广播的方式在发送端所处的自网内传输,所有主机接收后拆开包,发现目标ip为自己的,就响应,返回自己的mac
 36 
ARP协议
  1 传输层的由来:网络层的ip帮我们区分子网,以太网层的mac帮我们找到主机,然后大家使用的都是应用程序,你的电脑上可能同时开启qq,暴风影音,等多个应用程序,
  2 
  3 那么我们通过ip和mac找到了一台特定的主机,如何标识这台主机上的应用程序,答案就是端口,端口即应用程序与网卡关联的编号。
  4 
  5 传输层功能:建立端口到端口的通信
  6 
  7 补充:端口范围0-65535,0-1023为系统占用端口
  8 
  9 tcp协议:
 10 
 11 可靠传输,TCP数据包没有长度限制,理论上可以无限长,但是为了保证网络的效率,通常TCP数据包的长度不会超过IP数据包的长度,以确保单个TCP数据包不必再分割。
 12 
 13 udp协议:
 14 
 15 不可靠传输,”报头”部分一共只有8个字节,总长度不超过65,535字节,正好放进一个IP数据包。
传输层
  1 应用层由来:用户使用的都是应用程序,均工作于应用层,互联网是开发的,大家都可以开发自己的应用程序,数据多种多样,必须规定好数据的组织形式
  2 
  3 应用层功能:规定应用程序的数据格式。
  4 
  5 例:TCP协议可以为各种各样的程序传递数据,比如Email、WWW、FTP等等。那么,必须有不同协议规定电子邮件、网页、FTP数据的格式,这些应用程序协议就构成了”应用层”。
应用层

三、socket概念

能够唯一标示网络中的进程后,它们就可以利用socket进行通信了,什么是socket呢?

Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。

  1 也有人将socket说成ip+port,ip是用来标识互联网中的一台主机的位置,而port是用来标识这台机器上的一个应用程序,ip地址是配置到网卡上的,而port是应用程序开启的,ip与port的绑定就标识了互联网中独一无二的一个应用程序
  2 
  3 而程序的pid是同一台机器上不同进程或者线程的标识
socket = ip+port
  1 基于文件类型的套接字家族
  2 套接字家族的名字:AF_UNIX
  3 
  4 unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信
  5 
  6 基于网络类型的套接字家族
  7 套接字家族的名字:AF_INET
  8 
  9 (还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET)
套接字分类

套接字函数

服务端套接字函数
s.bind() 绑定(主机,端口号)到套接字
s.listen() 开始TCP监听
s.accept() 被动接受TCP客户的连接,(阻塞式)等待连接的到来

客户端套接字函数
s.connect() 主动初始化TCP服务器连接
s.connect_ex() connect()函数的扩展版本,出错时返回出错码,而不是抛出异常

公共用途的套接字函数
s.recv() 接收TCP数据
s.send() 发送TCP数据(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完)
s.sendall() 发送完整的TCP数据(本质就是循环调用send,sendall在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完)
s.recvfrom() 接收UDP数据
s.sendto() 发送UDP数据
s.getpeername() 连接到当前套接字的远端的地址
s.getsockname() 当前套接字的地址
s.getsockopt() 返回指定套接字的参数
s.setsockopt() 设置指定套接字的参数
s.close() 关闭套接字

面向锁的套接字方法
s.setblocking() 设置套接字的阻塞与非阻塞模式
s.settimeout() 设置阻塞套接字操作的超时时间
s.gettimeout() 得到阻塞套接字操作的超时时间

面向文件的套接字的函数
s.fileno() 套接字的文件描述符
s.makefile() 创建一个与该套接字相关的文件
  1 tcp是基于链接的,必须先启动服务端,然后再启动客户端去链接服务端
  2 
  3 server端
  4 
  5 import socket
  6 sk = socket.socket()
  7 sk.bind(('127.0.0.1',8898))  #把地址绑定到套接字
  8 sk.listen()          #监听链接
  9 conn,addr = sk.accept() #接受客户端链接
 10 ret = conn.recv(1024)  #接收客户端信息
 11 print(ret)       #打印客户端信息
 12 conn.send(b'hi')        #向客户端发送信息
 13 conn.close()       #关闭客户端套接字
 14 sk.close()        #关闭服务器套接字(可选)
 15 
 16 client端
 17 
 18 import socket
 19 sk = socket.socket()           # 创建客户套接字
 20 sk.connect(('127.0.0.1',8898))    # 尝试连接服务器
 21 sk.send(b'hello!')
 22 ret = sk.recv(1024)         # 对话(发送/接收)
 23 print(ret)
 24 sk.close()            # 关闭客户套接字
基于TCP协议的socket
  1 udp是无链接的,启动服务之后可以直接接受消息,不需要提前建立链接
  2 
  3 server端
  4 
  5 import socket
  6 udp_sk = socket.socket(type=socket.SOCK_DGRAM)   #创建一个服务器的套接字
  7 udp_sk.bind(('127.0.0.1',9000))        #绑定服务器套接字
  8 msg,addr = udp_sk.recvfrom(1024)
  9 print(msg)
 10 udp_sk.sendto(b'hi',addr)                 # 对话(接收与发送)
 11 udp_sk.close()                         # 关闭服务器套接字
 12 
 13 client端
 14 
 15 import socket
 16 ip_port=('127.0.0.1',9000)
 17 udp_sk=socket.socket(type=socket.SOCK_DGRAM)
 18 udp_sk.sendto(b'hello',ip_port)
 19 back_msg,addr=udp_sk.recvfrom(1024)
 20 print(back_msg.decode('utf-8'),addr)
基于UDP协议的socket
 
  1 from socket import *
  2 from time import strftime
  3 
  4 ip_port = ('127.0.0.1', 9000)
  5 bufsize = 1024
  6 
  7 tcp_server = socket(AF_INET, SOCK_DGRAM)
  8 tcp_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
  9 tcp_server.bind(ip_port)
 10 
 11 while True:
 12     msg, addr = tcp_server.recvfrom(bufsize)
 13     print('===>', msg)
 14 
 15     if not msg:
 16         time_fmt = '%Y-%m-%d %X'
 17     else:
 18         time_fmt = msg.decode('utf-8')
 19     back_msg = strftime(time_fmt)
 20 
 21     tcp_server.sendto(back_msg.encode('utf-8'), addr)
 22 
 23 tcp_server.close()
时间服务器server
  1 from socket import *
  2 ip_port=('127.0.0.1',9000)
  3 bufsize=1024
  4 
  5 tcp_client=socket(AF_INET,SOCK_DGRAM)
  6 
  7 
  8 
  9 while True:
 10     msg=input('请输入时间格式(例%Y %m %d)>>: ').strip()
 11     tcp_client.sendto(msg.encode('utf-8'),ip_port)
 12 
 13     data=tcp_client.recv(bufsize)
时间服务器client

四、黏包

同时执行多条命令之后,得到的结果很可能只有一部分,在执行其他命令的时候又接收到之前执行的另外一部分结果,这种显现就是黏包。

发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。

所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。

两种情况下会发生粘包。

发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)

  1 from socket import *
  2 ip_port=('127.0.0.1',8080)
  3 
  4 tcp_socket_server=socket(AF_INET,SOCK_STREAM)
  5 tcp_socket_server.bind(ip_port)
  6 tcp_socket_server.listen(5)
  7 
  8 conn,addr=tcp_socket_server.accept()
  9 
 10 data1=conn.recv(10)
 11 data2=conn.recv(10)
 12 
 13 print('----->',data1.decode('utf-8'))
 14 print('----->',data2.decode('utf-8'))
 15 
 16 conn.close()
server
  1 import socket
  2 BUFSIZE=1024
  3 ip_port=('127.0.0.1',8080)
  4 
  5 s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
  6 res=s.connect_ex(ip_port)
  7 
  8 
  9 s.send('hello'.encode('utf-8'))
 10 s.send('feng'.encode('utf-8'))
client

接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)

  1 from socket import *
  2 ip_port=('127.0.0.1',8080)
  3 
  4 tcp_socket_server=socket(AF_INET,SOCK_STREAM)
  5 tcp_socket_server.bind(ip_port)
  6 tcp_socket_server.listen(5)
  7 
  8 
  9 conn,addr=tcp_socket_server.accept()
 10 
 11 
 12 data1=conn.recv(2) #一次没有收完整
 13 data2=conn.recv(10)#下次收的时候,会先取旧的数据,然后取新的
 14 
 15 print('----->',data1.decode('utf-8'))
 16 print('----->',data2.decode('utf-8'))
 17 
 18 conn.close()
server
  1 import socket
  2 BUFSIZE=1024
  3 ip_port=('127.0.0.1',8080)
  4 
  5 s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
  6 res=s.connect_ex(ip_port)
  7 
  8 s.send('hello feng'.encode('utf-8'))
server

为何tcp是可靠传输,udp是不可靠传输

tcp在数据传输时,发送端先把数据发送到自己的缓存中,然后协议控制将缓存中的数据发往对端,对端返回一个ack=1,发送端则清理缓存中的数据,对端返回ack=0,则重新发送数据,所以tcp是可靠的

而udp发送数据,对端是不会返回确认信息的,因此不可靠

send(字节流)和recv(1024)及sendall

recv里指定的1024意思是从缓存里一次拿出1024个字节的数据

send的字节流是先放入己端缓存,然后由协议控制将缓存内容发往对端,如果待发送的字节流大小大于缓存剩余空间,那么数据丢失,用sendall就会循环调用send,数据不会丢失

黏包的解决方法

  1 import socket,struct,json
  2 import subprocess
  3 phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
  4 phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #就是它,在bind前加
  5 
  6 phone.bind(('127.0.0.1',8080))
  7 
  8 phone.listen(5)
  9 
 10 while True:
 11     conn,addr=phone.accept()
 12     while True:
 13         cmd=conn.recv(1024)
 14         if not cmd:break
 15         print('cmd: %s' %cmd)
 16 
 17         res=subprocess.Popen(cmd.decode('utf-8'),
 18                              shell=True,
 19                              stdout=subprocess.PIPE,
 20                              stderr=subprocess.PIPE)
 21         err=res.stderr.read()
 22         print(err)
 23         if err:
 24             back_msg=err
 25         else:
 26             back_msg=res.stdout.read()
 27 
 28 
 29         conn.send(struct.pack('i',len(back_msg))) #先发back_msg的长度
 30         conn.sendall(back_msg) #在发真实的内容
 31 
 32     conn.close()
 33 
 34 服务端(自定制报头)
server
  1 import socket,time,struct
  2 
  3 s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
  4 res=s.connect_ex(('127.0.0.1',8080))
  5 
  6 while True:
  7     msg=input('>>: ').strip()
  8     if len(msg) == 0:continue
  9     if msg == 'quit':break
 10 
 11     s.send(msg.encode('utf-8'))
 12 
 13 
 14 
 15     l=s.recv(4)
 16     x=struct.unpack('i',l)[0]
 17     print(type(x),x)
 18     # print(struct.unpack('I',l))
 19     r_s=0
 20     data=b''
 21     while r_s < x:
 22         r_d=s.recv(1024)
 23         data+=r_d
 24         r_s+=len(r_d)
 25 
 26     # print(data.decode('utf-8'))
 27     print(data.decode('gbk')) #windows默认gbk编码
 28 
 29 客户端(自定制报头)
client
我们可以把报头做成字典,字典里包含将要发送的真实数据的详细信息,然后json序列化,然后用struck将序列化后的数据长度打包成4个字节(4个自己足够用了)

发送时:
先发报头长度
再编码报头内容然后发送
最后发真实内容

接收时:
先手报头长度,用struct取出来
根据取出的长度收取报头内容,然后解码,反序列化
从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容

五、socketserver

import socketserver
class Myserver(socketserver.BaseRequestHandler):
    def handle(self):
        self.data = self.request.recv(1024).strip()
        print("{} wrote:".format(self.client_address[0]))
        print(self.data)
        self.request.sendall(self.data.upper())

if __name__ == "__main__":
    HOST, PORT = "127.0.0.1", 9999

    # 设置allow_reuse_address允许服务器重用地址
    socketserver.TCPServer.allow_reuse_address = True
    # 创建一个server, 将服务地址绑定到127.0.0.1:9999
    server = socketserver.TCPServer((HOST, PORT),Myserver)
    # 让server永远运行下去,除非强制停止程序
    server.serve_forever()
server
import socket

HOST, PORT = "127.0.0.1", 9999
data = "hello"

# 创建一个socket链接,SOCK_STREAM代表使用TCP协议
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
    sock.connect((HOST, PORT))          # 链接到客户端
    sock.sendall(bytes(data + "
", "utf-8")) # 向服务端发送数据
    received = str(sock.recv(1024), "utf-8")# 从服务端接收数据

print("Sent:     {}".format(data))
print("Received: {}".format(received))
client
原文地址:https://www.cnblogs.com/penghengshan/p/10826028.html