【网络编程】第2回 socket套接字和黏包

1 OSI各层特征

1.1 传输层

1.1.1 PORT协议

端口协议:规定了一台计算机上的每一个正在运行的应用程序都必须有一个端口号,端口号相当于是计算机用来管理多个应用程序的标记

1.1.2 本质

规定了数据传输所遵循的规则,数据传输能够遵循的协议有很多,TCP和UDP是较为常见的两个

1.1.3 TCP协议

1.1.3.1 定义

TCP--传输控制协议,提供是面向连接、可靠的字节流服务。当客户和服务器彼此交换数据前,必须先在双方之间建立一个TCP连接,之后才能传输数据。TCP提供超时重发,丢弃重复数据,检验数据,流量控制等功能,保证数据能从一端传到另一端。

1.1.3.2 三次握手

  1. 建立双向通道

  2. 洪水攻击:同时让大量客户端朝服务端发送建立TCP连接的请求

1.1.3.3 四次挥手

  1. 断开双向通道,中间的两步不能合并(需要有检查的时间)

1.1.3.4 判断

基于TCP传输数据非常的安全 因为有双向通道?

  1. 前半句对,后半句错
  2. 基于TCP传输数据,数据不容易丢失,不容易丢失的原因在于二次确认机制,每次发送数据都需要返回确认消息 否则在一定的时间会反复发送

1.1.4 UDP协议

1.1.4.1 定义

UDP--用户数据报协议,是一个简单的面向数据报的运输层协议。UDP不提供可靠性,它只是把应用程序传给IP层的数据报发送出去,但是并不能保证它们能到达目的地。由于UDP在传输数据报前不用在客户和服务器之间建立一个连接,且没有超时重发等机制,故而传输速度很快

1.1.4.2 总结

基于UDP协议发送数据,没有任何的通道也没有任何的限制,UDP发送数据没有TCP安全(没有二次确认机制)

1.1.1.3 对比例

  1. TCP类似于打电话:你一句我一句 有来有往
  2. UDP类似于发短信:只要发送了 不管别人看没看到 也不管回不回复

3. 应用层

  1. 主要取决于程序员自己采用什么策略和协议
    常见协议有:HTTP HTTPS FTP...

2. scoket套接字

2.1 基于文件类型的套接字家族

  1. 套接字家族的名字:AF_UNIX
  2. unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信

2.2 基于网路类型的套接字家族

  1. 套接字家族的名字:AF_INET
  2. 所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET

2.3 信息交互

2.3.1 服务端代码

import socket


# 1.创捷一个socket对象
server = socket.socket()  # 括号内什么都不写,默认就是基于网络的TCP套接字
# 2.绑定一个固定的地址(ip\port)
server.bind(('127.0.0.1', 8080))  # 127.0.0.1本地回环地址(只允许自己的机器访问)
# 3.半连接池
server.listen(5)
# 4.开业 等待接客
sock, address = server.accept()
print(sock, address)  # sock是双向通道,address是客户端地址
# 5.数据交互
sock.send(b'hello big baby')  # 朝客户端发送数据
data = sock.recv(1024)  # 接收客户端发送的数据 1024bytes
print(data)
# 断开连接
sock.close()  # 断链接
server.close()  # 关机

2.3.2 客户端代码

import socket

# 1.产生一个socket对象
client = socket.socket()
# 2.连接服务端(拼接服务端的ip和port)
client.connect(('127.0.0.1', 8080))
# 3.数据交互
data = client.recv(1024)  # 接收服务端发送的数据
print(data)
client.send(b'hello sweet server')  # 朝服务端发送数据
# 4. 关闭
client.close()

2.3.3 运行展示

  1. 先运行服务端
  2. 在运行客户端
  3. 传信息再到服务端里查看

3. 代码优化

3.1 代码优化讲解

3.1.1 send与recv

  1. 客户端与服务端不能同时执行同一个
  2. 有一个收,另外一个就是发
  3. 有一个发,另外一个就是收
  4. 不能同时收或者发

3.1.2 消息自定义

input获取用户数据即可(主要编码解码)

3.1.3 循环通信

给数据交互环节添加循环即可

3.1.4 服务端能够持续提供服务

不会因为客户端断开连接而报错,异常捕获 一旦客户端断开连接 服务端结束通信循环

3.1.5 消息不能为空

判断是否为空 如果是则重新输入(主要针对客户端)

3.1.6 服务端频繁重启可能会被占用的错(主要针对mac电脑)

from socket import SOL_SOCKET,SO_REUSEADDR
server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加

3.1.7 客户端异常退出会发送空消息(针对mac linux)

针对接收的消息加判断处理即可

3.2 代码优化展示

3.2.1 服务端

import socket
from socket import SOL_SOCKET, SO_REUSEADDR

# 1.创建一个socket对象
server = socket.socket()  # 括号内什么都不写 默认就是基于网络的TCP套接字
# 2.绑定一个固定的地址(ip\port)
server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)  # 就是它,在bind前加
server.bind(('127.0.0.1', 8080))  # 127.0.0.1本地回环地址(只允许自己的机器访问)
# 3.半连接池(暂且忽略)
server.listen(5)
# 4.开业 等待接客
while True:
    sock, address = server.accept()
    print(sock, address)  # sock是双向通道 address是客户端地址
    # 5.数据交互
    while True:
        try:
            msg = input('请输入发送给客户端的消息>>>:').strip()
            if len(msg) == 0: continue
            sock.send(msg.encode('utf8'))  # 朝客户端发送数据
            data = sock.recv(1024)  # 接收客户端发送的数据 1024bytes
            if len(data) == 0:  #
                break
            print(data.decode('utf8'))
        except ConnectionResetError:
            sock.close()
            break

3.2.2 客户端

import socket

# 1.产生一个socket对象
client = socket.socket()
# 2.连接服务端(拼接服务端的ip和port)
client.connect(('127.0.0.1', 8080))
# 3.数据交互
while True:
    data = client.recv(1024)  # 接收服务端发送的数据
    print(data.decode('utf8'))
    msg = input('请输入发送给客户端的消息>>>:').strip()
    if len(msg) == 0:
        msg = '手抖了一下 暂无消息'
    client.send(msg.encode('utf8'))  # 朝服务端发送数据

3.3.3 运行结果展示


4. 半连接池

server.listen(5)
主要是为了做缓冲 避免太多无效等待

5. 黏包

5.1 定义

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

5.2 舔包现象

5.2.1 服务端代码

import socket

# 1.创建一个socket对象
server = socket.socket()  # 括号内什么都不写 默认就是基于网络的TCP套接字
# 2.绑定一个固定的地址(ip\port)
server.bind(('127.0.0.1', 8080))  # 127.0.0.1本地回环地址(只允许自己的机器访问)
server.listen(5)

sock, address = server.accept()

print(sock.recv(5))
print(sock.recv(5))
print(sock.recv(5))

5.2.2 客户端代码

import socket

# 1.产生一个socket对象
client = socket.socket()
# 2.连接服务端(拼接服务端的ip和port)
client.connect(('127.0.0.1', 8080))


client.send(b'jason')
client.send(b'kevin')
client.send(b'jerry')

5.2.3 结果展示


5.2.4 结论

  1. 只有TCP有粘包现象,UDP永远不会粘包
  2. TCP特性>>流式协议:所有的数据类似于水流,连接在一起的。ps:数据量很小,并且时间间隔很多,那么就会自动组织到一起
  3. recv:我们不知道即将要接收的数据量多大,如果知道的话不会产生也不产生黏包

5.3 struct 模块

5.3.1 代码讲解

import struct

info = '下午上课 以后可能是常态!'
print(len(info))  # 13  数据原本的长度
res = struct.pack('i', len(info))  # 将数据原本的长度打包
print(len(res))  # 4  打包之后的长度是4
ret = struct.unpack('i', res)  # 将打包之后固定长度为4的数据拆包
print(ret[0])  # 13  又得到了原本数据的长度

info1 = '打起精神啊 下午也需要奋斗 也需要认真听 客服困难 你困我也困!!!'
print(len(info1))  # 34
res = struct.pack('i', len(info1))  # 将数据原本的长度打包
print(len(res))  # 4  打包之后的长度是4
ret = struct.unpack('i', res)
print(ret[0])  # 34

5.3.2 总结 思路

  1. struct模块无论数据长度是多少,都可以帮你打包成固定长度,然后基于该固定长度 还可以反向解析出真实长度
  2. 思路
1. 先将真实数据的长度制作成固定长度 
2. 发送送固定长度的报头
3. 再发送真实数据
1. 先接收固定长度的报头
2. 再根据报头解压出真实长度
3. 根据真实长度接收即可
  1. struct模块针对数据量特别打的数字没有办法打包

5.3.3 图表

6.解决黏包问题的终极方案

6.1 服务端

  1. 先构造一个数据的详细字典
  2. 对字典数据进行打包处理,得到一个客户端固定长度的数据
  3. 将上述打包之后的数据发送给客户端
  4. 将字典数据发给客户端
  5. 将真实数据发送给客户端

6.2 客户端

  1. 先接收固定长度的数据
  2. 根据固定长度解析出即将要接收的字典真实长度
  3. 接收字典数据
  4. 根据字典数据,获取出真实的数据的长度
  5. 接收真实数据长度
posted @   |相得益张|  阅读(46)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示