TCP与UDP协议,socket套接字编程,通信相关操作

本文深入探讨了TCP与UDP协议的工作原理,包括TCP的三次握手和四次挥手过程,以及UDP的简单不可靠传输特性。此外,还介绍了应用层中的Socket套接字编程,展示了TCP服务器与客户端的简单交互示例,并讨论了循环通信、半连接池和粘包问题的解决方案。通过对TCP与UDP区别的分析,帮助读者理解在不同场景下选择合适协议的重要性。
摘要由CSDN通过智能技术生成

TCP与UDP协议

  • 本质

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

TCP协议

TCP是一种面向广域网的通信协议,目的是在跨越多个网络通信时,为两个通信端点之间提供一条通信方式

三次握手

建立双向通道,中间的 FIN 和 ACK 能合并在一起
ps:洪水攻击(同时让大量的客户端朝服务端建立TCP连接的请求,容易出现状况)

在这里插入图片描述

第一次握手:客户端将标志位SYN设置为1,并且随机产生一个值seq=x,并且将该数据包发送给服务器,此刻客户端进入等待状态,等待服务器的确认
第二次握手:服务器接收到数据包之后,标志位是SYN=1,可以知道客户端请求连接,服务端如果同意创建连接,此刻SYN ACK都设置为1,并且ack=x+1,随机产生一个值seq=y,传递给客户端
第三次握手:客户端接收到确认之后,检查ack是否是x+1,ACK是否是1,如果都是正确的,会将标志位ACK设置为1,确认号ack设置为y+1,发送给服务端

四次挥手

断开双向通道
双方各自向对方发起建立连接的请求,再各自给对方回应,只不过,中间的 FIN 和 ACK 不一定能合并在一起

在这里插入图片描述

第一次挥手:客户端发送标识位FIN,用来关闭客户端到服务端的连接
第二次挥手:服务器接收到客户端发送的FIN之后,先发送ACK,ack,seq给客户端
第三次挥手:服务器发送FIN给客户端,用来关闭服务器和客户端的数据传送
第四次挥手:客户端接收到FIN之后,发送ACK,ack给服务端
注意:TCP链接是全双工的 ,全双工同时可以在2个方向上传输数据

类似于打电话的一个过程:
在这里插入图片描述

UDP协议

  • 本质

    基于UDP协议发送数据,没有任何的通道也没有任何的限制,提供面向交易的简单和不可靠的信息传输服务

  • 作用

    主要用于支持需要在计算机之间传输数据的网络应用

TCP与UDP的区别

1.UDP和TCP协议的主要区别是两者在如何实现信息的可靠传递方面不同
2.TCP 是面向连接的传输控制协议,而UDP 提供了无连接的数据报服务;
3.TCP 具有高可靠性,确保传输数据的正确性,不出现丢失或乱序;UDP 在传输数据前不建立连接,不对数据报进行检查与修改,无须等待对方的应答,所以会出现分组丢失、重复、乱序;
4.UDP 具有较好的实时性,工作效率较 TCP 协议高;UDP 段结构比 TCP 的段结构简单

应用层

1.应用层的目的:
  
  应用层对应用程序的通信提供服务。

2.应用层重要协议:
  
  FTP、SMTP和POP3、HTTP、DNS。

3.应用层的功能:

① 文件传输。访问和管理。
② 电子邮件。
③ 虚拟终端。
④ 查询服务和远程作业登录。

socket套接字

Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。

基于文件类型的套接字家族
	套接字家族的名字:AF_UNIX
基于网络类型的套接字家族
	套接字家族的名字:AF_INET

理解图:

在这里插入图片描述

运行程序的时候 肯定是先确保服务端运行 之后才是客户端

服务端:

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)
# 6.断开连接
sock.close()  # 断链接
server.close()  # 关机

客户端:

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()
  • 服务端套接字函数
    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.先解决消息固定的问题
    解决办法:利用input获取用户输入
    2.再解决通信循环的问题
    解决方法:将双方用于数据交互的代码循环起来

    代码实现

    while True:
        data = sock.recv(1024)  # 听别人说话
        print(data.decode('utf8'))
        msg = input('请回复消息>>>:').strip()
        sock.send(msg.encode('utf8'))  # 回复别人说的话
     
    while True:
        msg = input('请输入你需要发送的消息>>>:').strip()
        client.send(msg.encode('utf8'))  # 给服务端发送消息
        data = client.recv(1024)  # 接收服务端回复的消息
        print(data.decode('utf8'))
    
  • 输入的消息不能为空(主要是针对客户端)

    解决方法:判断是否为空,为空则重新输入
    
  • 服务端能够持续提供服务

    需求:不会因为客户端断开连接而报错
    解决方法:异常捕获 一旦客户端断开连接 服务端结束通信循环 调到连接处等待
    

半连接池

1.什么是半连接池:

当服务器在响应了客户端的第一次请求后会进入等待状态,会等客户端发送的ack信息,这时候这个连接就称之为半连接

2.本质

半连接池其实就是一个容器,系统会自动将半连接放入这个容器中,可以避免半连接过多而保证资源耗光

3.产生半连接的两种情况:

客户端无法返回ACK信息
服务器来不及处理客户端的连接请求

粘包问题

问题:

我们知道tcp容易产生黏包的问题,而udp不会产生黏包的问题,但是会产生丢包的问题,tcp应用的场景很多所以黏包问题必须要解决。(传输信息字节大小和允许接收信息字节大小的问题)

1.TCP特性

流式协议:所有的数据类似于水流 连接在一起的
ps:数据量很小 并且时间间隔很多 那么就会自动组织到一起

2.recv

我们不知道即将要接收的数据量多大 如果知道的话不会产生也不会产生黏包
  • 解决方法一

    1.解决黏包问题第一种方法,我们知道黏包问题是由于tcp的优化算法将两个不太大的数据包合并了一起发送的,这种情况一般出现在连续使用几个send()出现的,所以我们如果知道要发送的数据有多大我们就可以设置接收的大小,这样就可以刚好能把所有的数据接收完。下面是具体的步骤细节见代码

这是远程执行cmd命令并返回结果的程序
server端代码
import struct
import socket
sk = socket.socket()
sk.bind(('127.0.0.1',8080))
sk.listen()
conn,addr = sk.accept()
while True:
    cmd = input('>>>')
    conn.send(bytes(cmd,encoding='utf-8'))
    num = conn.recv(1024).decode('utf-8')   #接收client端计算好的数据长度
    conn.send(bytes('ok',encoding='utf-8'))
    #发送一个确认防止发送num的时候跟后面的send内容合并了
    ret = conn.recv(num)
    print(ret.decode('gbk'))
conn.close()
sk.close()
client端代码
import socket
import struct
import subprocess
sk = socket.socket()
sk.connect(('127.0.0.1',8080))
while True:
    cmd = sk.recv(1024).decode('utf-8')
    ret = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
    std_out = ret.stdout.read()
    std_err = ret.stderr.read()
    sk.send(bytes(str(len(std_err)+len(std_out)),encoding='utf-8'))
    #上面计算字符串的长度发送给server端在接收的时候刚好接收那么长的数据
    sk.recv(1024) #ok  这一步主要的目的是为了将num的发送跟后面的send分割开防止黏包现象
    sk.send(std_out)
    sk.send(std_err)
sk.close()

总结:但是有一个问题就是多了一次连接延时要接收一个没有用的数据 ok,如何不需要接收ok就能解决黏包现象呢?这就需要下面这种解决方案.

  • 解决方法二

    2.用struct模块解决黏包现象

server端代码

#tcp黏包现象的解决 struct
import struct
import socket
sk = socket.socket()
sk.bind(('127.0.0.1',8080))
sk.listen()
conn,addr = sk.accept()
while True:
    cmd = input('>>>')
    conn.send(bytes(cmd,encoding='utf-8'))
    num = conn.recv(1024)   #接收数据
    num = struct.unpack('i',num)[0]#进行解包,解包的结果是一个元组类型取第一个数据
    ret = conn.recv(num)
    print(ret.decode('gbk'))
conn.close()
sk.close()
client端代码

import socket
import struct
import subprocess
sk = socket.socket()
sk.connect(('127.0.0.1',8080))
while True:
    cmd = sk.recv(1024).decode('utf-8')
    ret = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
    std_out = ret.stdout.read()
    std_err = ret.stderr.read()
    num = len(std_err) + len(std_out)
    num = struct.pack('i',num)  #利用struct模块将一个数据转换成bytes类型 i代表int型
    sk.send(num)
    sk.send(std_out)
    sk.send(std_err)
sk.close()

总结:
struct模块无论数据长度是多少 都可以帮你打包成固定长度
然后基于该固定长度 还可以反向解析出真实长度
struct模块针对数据量特别大的数字没有办法打包!!!

思路:

服务端

1.先将真实数据的长度制作成固定长度 4
2.先发送固定长度的报头
3.再发送真实数据

客户端

1.先接收固定长度的报头 4
2.再根据报头解压出真实长度
3.根据真实长度接收即可

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值