七层协议、TCP(粘包-多路复用)、UDP

news/2025/2/9 4:21:44

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:路由器 交换机都可以称之为网关!
 
View Code

 

 

第四层.传输层(重点)

  传输层的由来

    通过物理层建立链接通道

    通过数据链路层的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是同一台机器上不同进程或者线程的标识
socket说成ip+port
socket()模块函数用法

 

  什么时候使用

    需要开发一款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()
socket单发单次交流

 

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()
带连接循环和通讯循环的TCP通讯模板

 

 

 

 

服务端套接字函数
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的工作方式

  1. TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
  2. UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
  3. 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
模拟SSH实现远程执行命令--服务端
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"))
模拟SSH实现远程执行命令--客户端

   怎么处理粘包问题:先发送长度信息,再发生真实数据,

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)
View Code

 

 

 

 

 

 

 

 

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)
简单UDP格式

  对比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字节)

 

转载于:https://www.cnblogs.com/pdun/p/10450464.html


http://www.niftyadmin.cn/n/3614044.html

相关文章

4.8 实例:矩阵乘法

实例:矩阵乘法 问题分析: 算法描述: 编程: #include<iostream> using namespace std;

4.9 实例:取子字符串

实例:取子字符串 ----用户输入一个字符串,然后输入起始位置k和长度l ----显示从第k个字符开始,长度为l的子字符串 ----要求字符串输入一次,子串操作可以多次,输入位置和长度均为0时停止 问题分析: 算法

Python版本,图片,视频断点续传下载

2019独角兽企业重金招聘Python工程师标准>>> 图片下载 tqdm tqdm是一个快速、扩展性强的进度条工具库&#xff0c;用户只需要封装任意的迭代器 tqdm(iterator)&#xff0c;tqdm官方文档。 对于爬虫进度的监控&#xff0c;这是个不错的工具。 requests模块实现下载 对…

4.10 实例:词频统计

实例:词频统计 ----输入一系列英文单词(单词之间用空格隔开),用“xyz”表示输入结束 ----统计各单词出现的次数(单词不区分大小写),对单词按字典顺序进行排序后输出单词和词频 运行结果: 问题分析:

5.2 函数的定义

函数定义的一般格式: 2. 函数值类型----即函数的返回值类型 (1)返回简单类型,如int,long,float,double,char等 (2)返回结构类型 (3)返回指针类型 (4)返回引用类型 注意:如果函数没有任何返回值,这时函数的返回值类型应标记为void。void类型称为无类型和…

为什么要停止使用RSA密钥交换?

今天我们将讨论为什么你应该停止使用RSA密钥交换。SSL/TLS生态系统的最大弱点之一是其向后兼容性。上周&#xff0c;有六位研究人员发表了一篇论文&#xff0c;详细介绍了一种名为Bleichenbacher CAT的旧漏洞的新变种&#xff0c;该漏洞强调了这种弱点。 那么&#xff0c;让我们…

PYTHON2.day13

前情回顾 1.查找操作 find(query,field) findOne(query,field) 2.query操作符 比较:$eq $lt $gt $lte $gte $in $nin 逻辑&#xff1a;$and $or $not $nor 数组&#xff1a;$all $size 其他&#xff1a;$exists $mod $type 3.数据处理函数 p…

5.3 函数的声明

函数的声明&#xff1a; &#xff08;1&#xff09;在C中&#xff0c;程序编译的单位是源程序文件&#xff08;即源文件&#xff09;&#xff0c;一个由多个函数构成的程序可以组织存放在一个或多个源文件中。 &#xff08;2&#xff09;在源文件中&#xff0c;函数之间的排列…