ip + mac可以识别全世界范围内独一无二的一台计算机
port可以标识一台计算机之上的应用软件
网络编程的目的就是开发出一款基于CS结构的应用程序
一、C/S架构
提供数据的一方称之为服务器(Server),访问数据的一方称之为客户端(Client)(基于浏览器B/S)
二、网络通讯的基本要素
两台计算机要通讯,必须要具备两个基本要素
1、物理连接介质(包括网线,无线电,光纤等)
2、通讯协议
三、网络通讯协议
OSI七层协议
1、什么是OSI 开放式系统互联通信参考模型
7层 | 数据格式 | 功能与连接方式 | 典型设备 |
应用层Application | 网络服务于使用者应用程序间的一个接口 | ||
表示层Presentation | 数据表示、数据安全、数据压缩 | ||
会话层Session | 建立、管理和终止会话 | ||
传输层Ttransport | 数据组织成数据段Segment | 用一个寻址机制来标识一个特定的应用程序(端口号) | 四层交换机、四层路由器 |
网络层Network | 分割和重新组合数据报Packet | 基于网路层地址(IP地址)进行不同网络系统间的路径选择 | 路由器、三层交换机 |
数据链路层Data Link | 将比特信息封装成数据帧Frame | 通过使用接收系统放硬件地址或物理地址寻址 | 网桥、交换机、网卡 |
物理层Physicai | 传输比特(bit)流 | 建立、维护和取消物理连接 | 光纤、同轴电缆、双绞线、中继器和集线器 |
==========osi七层结构========
第一层、物理层 规定物理介质的相关规范(电缆,光纤) 物流层的功能:基于电子器件发送电流信号,根据电流高低对应0、1,也就是二进制位 它的问题是:对方不知道二进制到底什么含义,每一次到底读多少位二进制 第二层、数据链路层 规定一组电信号有多少位 每组电信号包含什么样的内容 每台电脑必须拥有一个全球唯一的MAC地址(可以有多个) 通过广播的方式来找到对方的MAC地址 问题是:不可能全球广播,会造成广播风暴(广播太多,网络瘫痪) 第三层、网络层(IP协议)
IP协议是工作在网络层的协议,全称:Internet Protocol Address,翻译为互联网协议地址 路由协议:用于选择一条最短的传输路径 3.1 IP地址(重点) ip协议定义的地址称之为ip地址,IPV4,IPV6,广泛采用的是v4版本即ipv4,4段十进制0.0.0.0 范围0.0.0.0-255.255.255.255 一个ip地址通常写成四段十进制数,例:192.168.10.1 网络号:标识子网(前三段) 主机号:标识主机(后一段) IP地址的分类: A类保留给政府机构 10.0.0.1 - 10.255.255.254 B类分配给中等规模格式 172.16.0.1 - 172.31.255.254 C类分配给任何需要的人 192.168.0.1 - 192.168.255.254 我们的电脑ip通常都是C类的,以192.168开头,正因为C类任何人都可以用 D类用于组播 E类用于实验 3.2子网掩码 什么是子网掩码 所谓”子网掩码”,就是表示子网络特征的一个参数。它在形式上等同于IP地址,也是一个32位二进制数字,它的网络部分全部为1,主机部分全部为0。比如,IP地址172.16.10.1,如果已知网络部分是前24位,主机部分是后8位, 那么子网络掩码就是11111111.11111111.11111111.00000000,写成十进制就是255.255.255.0 为什么需要子网掩码 单纯的ip地址段只是标识了ip地址的种类,无法辨识一个ip所处的子网 总结一下,IP协议的作用主要有两个,一个是为每一台计算机分配IP地址,另一个是确定哪些地址在同一个子网络。 3.3、arp协议 ARP用广播的方式通过ip来获取MAC地址, 不在同一子网时 ARP得到的时对方网关的MAC地址,数据到达对方网关后,由网关根据IP交给对应的主机,当然对方网关获取主机MAC也是通过ARP ps:路由器 交换机都可以称之为网关!
第四层.传输层(重点)
传输层的由来
通过物理层建立链接通道
通过数据链路层的MAC,可以定位某个局域网中的某台主机
通过网络层的IP地址,子网掩码,可以定位到全球范围的某一个局域网下的某台主机
那么问题来了
一台计算机上运行着很多程序,比如同时运行着qq和微信,那么到底谁来接受数据呢
答案就是
端口号,端口是需要联网的应用程序与网卡关联的编号
传输层的功能:建立端口与端口的通信
端口范围0—65535,1—1023是系统占用端口(1023<可用<65535)
TCP协议
TCP协议 #可靠传输,TCP数据包没有长度限制,理论上可以无限长,但是为了网路效率,通常TCP数据包的长度不会超过ip数据包的长度,以确保 单个TCP数据包不必再分割 #TCP之所以可靠,是因为数据传输前需要三次握手确认建立链接 #三次握手与四次挥手 三次握手:确保传输通道是可用的 四次挥手:确保数据传输完毕 #TCP协议优点 优点:能够保证数据传输是完整的 缺点:由于每次都需要传输确认信息,导致传输效率降低 场景:多用于必须保证数据完整性的场景,例如文本信息,支付信息等!
客户端存在两种状态,syn和establisehd但可捕捉到establisehd,因为syn太快,
同理,服务端可捕捉到establisehd
如果服务端出现大量的syn_recv说明正在遭受洪水攻击
UDP协议
不可靠传输,“报头”部分一共只有8个字节,总长度不超过65535字节,正好放进一个IP数据包
UDP协议采取的方式与TCP完全不同,其根本不关心,对方是否收到数据,甚至不关心,对方的地址是否有效,只要将数据报发送到 网络,便什么都不管了
优点:由于不需要传输确认信息,所以传输效率高于TCP协议
缺点:传输数据可能不完整
场景:视频聊天,语音聊天等,不要求数据完整性,但是对传输速度要求较高
五、应用层
应用层由来:用户使用的都是应用程序,均工作于应用层,互联网是开放的,大家都可以开发自己的应用程序,用什么样的数据 格式来传输,就需要由应用程序开发者自己来制定
应用层功能:规定应用程序的数据格式。
例:TCP协议可以为各种各样的程序传递数据,比如Email、WWW、FTP等等。那么,必须有不同协议规定电子邮件、网页、 FTP数据的格式,这些应用程序协议就构成了”应用层”。
六、套接字发展及其分类
发展
套接字起源于20世纪70年代加利福尼亚大学伯克利分校的Unix,即人们所说的BSD Unix,一开始,套接字被设计用在同一台主机上的多个应用程序之间的通讯,这也被称为进程间通讯或IPC。套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的
基于文件型的套接字家族
套接字家族名字:AF_UNEX
unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接通信
基于网络类型的套接字家族
套接字家族名字:AF_INET
(还有AF_INET6被用于ipv6,还有一些其他家族,我们大多使用AF_INET)
常用的TCP/IP协议的3种套接字类型如下所示。 流式套接字(SOCK_STREAM): 流式套接字用于提供面向连接、可靠的数据传输服务。该服务将保证数据能够实现无差错、无重复发送,并按顺序接收。流式套接字之所以能够实现可靠的数据服务,原因在于其使用了传输控制协议,即TCP(The Transmission Control Protocol)协议。 数据报套接字(SOCK_DGRAM): 数据报套接字提供了一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。数据报套接字使用UDP(User Datagram Protocol)协议进行数据的传输。由于数据报套接字不能保证数据传输的可靠性,对于有可能出现的数据丢失情况,需要在程序中做相应的处理。 原始套接字(SOCK_RAW): 原始套接字(SOCKET_RAW)允许对较低层次的协议直接访问,比如IP、 ICMP协议,它常用于检验新的协议实现,或者访问现有服务中配置的新设备,因为RAW SOCKET可以自如地控制Windows下的多种协议,能够对网络底层的传输机制进行控制,所以可以应用原始套接字来操纵网络层和传输层应用。比如,我们可以通过RAW SOCKET来接收发向本机的ICMP、IGMP协议包,或者接收TCP/IP栈不能够处理的IP包,也可以用来发送一些自定包头或自定协议的IP包。网络监听技术很大程度上依赖于SOCKET_RAW 原始套接字与标准套接字(标准套接字指的是前面介绍的流式套接字和数据报套接字)的区别在于:原始套接字可以读写内核没有处理的IP数据包,而流式套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送数据必须使用原始套接字。
socket(中文名为套接字)
socket是什么
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它封装了传输层一堆协议的模块,留下简单的调用接口
也有人将socket说成ip+port,ip是用来标识互联网中的一台主机的位置,而port是用来标识这台机器上的一个应用程序,ip地址是配置到网卡上的,而port是应用程序开启的,ip与port的绑定就标识了互联网中独一无二的一个应用程序
而程序的pid是同一台机器上不同进程或者线程的标识
什么时候使用
需要开发一款C/S结构的应用程序时,就需要使用它
默认情况下创建的是基于网络的TCP协议的socket对象
服务器分大致流程
1、创建socket对象
2、绑定ip和端口
3、监听链接
4、接受请求
5、收发数据
6、断开链接
----------------------------------------------------------------------------
以手机为例
impoort socket
phone = socket.socket(socket.AF_INET , socket.SOCK_STMREAM) #SOCK_STMREAM = => TCP(流式协议)
phone.bind(("127.0.0.1,8080)) # 127.0.0.1测试专用,自动匹配自身,但仅自身可访问
3开机
phone.listen(5) #同一时刻最大请求数5个
4电话请求
coon,client_addr = phone.accent() #(双向链接的套接字对象 ,存放客户端ip和端口的小元组)
print(coon) #coon代表双向链接,用来收发消息
print(client_addr)
5收/发消息
data = conn.recv(1024) #1024接收的最大字节bytes
print("收到客户端数据",data)
coon.send(data.upper()) #发
6挂电话链接
coon.close
7关机
phone.close
服务器 from socket import * s = socket() s.bind(("127.0.0.1",8006)) s.listen() s,s_addr = s.accept() data = s.recv(1024) print(data) 客户端 from socket import * c = socket() c.connect(("127.0.0.1",8006)) c.send("hello".encode("utf-8")) c.close()
import socket s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.bind(("127.0.0.1",8080)) s.listen(5) while True: coon,client = s.accept() while True: try: data = coon.recv(1024) #注意是用coon if not data: print('客户端下线了') break print("收到数据",data) coon.send(data.upper()) #注意是coon except ConnectionResetError: print("客户端异常掉线") break ---------------------------------------------------------------------------- 客户端 import socket c = socket.socket(socket.AF_INET,socket.SOCK_STREAM) c.connect(("127.0.0.1",8080)) while True: msg = input(">>:q 退出") if not msg:continue if msg == "q":break c.send(msg.encode("utf-8")) data = c.recv(1024) print(data) c.close()
服务端套接字函数
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() 创建一个与该套接字相关的文件
七、粘包
学习粘包,首先看TCP,UDP的工作方式
- TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
- UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
- tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头.
什么是粘包
只有TCP有粘包现象,UDP永远不会粘包,所谓粘包,主要问题是接收方不知道消息的边界,不知道一次性取多少字节造成的
什么情况下会产生粘包
1、发送端需要等缓冲区满才能发送出去,造成粘包(发送数据时间间隔短,数据很小,会合到一起,产生粘包)
from socket import * s = socket() s.bind(("127.0.0.1",8181)) s.listen(5) coon,client_addr = s.accept() data = coon.recv(10).decode("utf-8") print(data)
from socket import * c = socket() c.connect(("127.0.0.1",8181)) #原本想一条一条的接收, c.send("hello".encode("utf-8")) c.send("world".encode("utf-8")) c.send("man".encode("utf-8"))
2、接收放不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再接收的时候,会从缓冲区拿上次遗留的数据,产生粘包)
from socket import * import subprocess s = socket() s.bind(("127.0.0.1",8888)) s.listen(5) while True: conn,client_addr = s.accept() while True: try: cmd = conn.recv(1024) if not cmd: print('用户端已下线') break # 得到一个对象obj obj = subprocess.Popen(cmd.decode("utf-8"), shell = True, stdout = subprocess.PIPE, stderr = subprocess.PIPE ) stdout = obj.stdout.read() stderr = obj.stderr.read() conn.send(stderr+stdout) except ConnectionResetError: print("客户端异常掉线") break
from socket import * c = socket() c.connect(("127.0.0.1",8888)) while True: cmd = input(">> q:退出") if not cmd: continue c.send(cmd.encode("utf-8")) cmd_res = c.recv(1024) print(cmd_res.decode("gbk"))
怎么处理粘包问题:先发送长度信息,再发生真实数据,
struct模块
由于报头与真实数据也会粘包,所以,需要将报头所占的字节数固定下来
这便需要用到struct模块(pack,unpack)
服务端 import socket,struct s = socket.socket() s.bind(("127.0.0.1",8888)) s.listen(5) conn,addr = s.accept() c_len_data = conn.recv(4) c_len = struct.unpack("i",c_len_data) print(c_len) #执行后 >>(28,) 输出的是元组类型,需要取第一个 客户端 import socket,json,struct c = socket.socket() c.connect(("127.0.0.1",8888)) c_dic = {"name":"pdun","age":"1"} c_data = json.dumps(c_dic).encode("utf-8") c_len = struct.pack("i",len(c_data)) c.send(c_len) c.send(c_data)
UDP
DUP是无链接的,先启动哪一端都不会报错
import socket s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) s.bind(("127.0.0.1",5666)) ip_prot = ("127.0.0.1",5666) while True: res,addr = s.recvfrom(1024) print(res) print(addr) -------------------------------------------------------------------------- 客户端 import socket c = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) ip_port = ("127.0.0.1",5666) while True: msg = input(">>:") c.sendto(msg.encode("utf-8"),ip_port) c.recvfrom(1024)
对比TCP
服务器:
UDP不需要监听和接受请求,
TCP服务默认只能与一个客户端进行通讯,下个客户端必须等到上个客户端断开链接
UDP多个客户端请求会被 依次处理,由于不需要建立链接,所以感觉好像是可以同时处理
客户端:
不需要建立链接,直接发送即可,可以发送空消息
当接收方缓冲区的长度小于数据报的长度时,Windows会报异常,而Linux不会,缓冲区多大就接收几个
import socket s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) s.bind(("127.0.0.1",8899)) res,addr = s.recvfrom(1024) #即便是1024远大于要发送信息,但仍只打印hello print(res,addr) ---------------------------------------------------------------------- 客户端 import socket client = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) ip_info = ("127.0.0.1",8899) client.sendto("hello".encode("utf-8"),ip_info) client.sendto("world".encode("utf-8"),ip_info) ----------------------------------------------------------------------- #如果将缓冲区的1024改为1,Windows会报错
基于UDP的聊天室练习
import socket s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) s.bind(("127.0.0.1",8866)) while True: res,addr = s.recvfrom(1024) print("收到来自%s的消息:%s" %(addr,res.decode("utf-8"))) msg1 = input(">>:") s.sendto(msg1.encode("utf-8"),addr) --------------------------------------------------------------------------------- 客户端一 import socket c = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) while True: msg = input(">>:") if msg =="q":break c.sendto(msg.encode("utf-8"),("127.0.0.1",8866)) res,addr = c.recvfrom(1024) print("收到来自%s的消息:%s" %(addr,res.decode("utf-8"))) ------------------------------------------------------------------------------------- 客户端二 import socket c = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) while True: msg = input(">>:") if msg =="q":break c.sendto(msg.encode("utf-8"),("127.0.0.1",8866)) res,addr = c.recvfrom(1024) print("收到来自%s的消息:%s" %(addr,res.decode("utf-8")))
基于UDP的时间服务器
import socket,time s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) s.bind(("127.0.0.1",8888)) while True: res,addr = s.recvfrom(1024) t = time.strftime(res.decode("utf-8"),time.localtime()) s.sendto(t.encode("utf-8"),addr) ------------------------------------------------------------------------- import socket c = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) addr = ("127.0.0.1",8888) fmt = "%Y-%m-%d %H:%M:%S" c.sendto(fmt.encode("utf-8"),addr) data,addr = c.recvfrom(1472) print(data.decode("utf-8"))
注意:UDP在使用时,必须保证接收方的数据缓冲区的大小,必须大于或等于发送方的数据报大小
由于缓冲区的大小不可能无限大,所以UDP不适用于数据量较大的情况,如果一定要使用UDP来传输较大数据量的时候,需要自己对数据进行切割,组装。(UDP最大为1472字节)