鄙人学习笔记
文章目录
- 套接字介绍
- 定义
- 套接字分类(针对TCP和UDP的分类)
- TCP套接字编程
- 服务端流程
- 代码实现
- 举个例子
- 客户端流程
- 代码实现
- 举个例子
- TCP套接字数据传输特点
- 做个练习
- 网络收发缓冲区
- 举个例子
- TCP粘包
套接字介绍
定义
套接字是实现网络编程进行数据传输的一种技术手段
套接字分类(针对TCP和UDP的分类)
①流式套接字(SOCK_STREAM): 以字节流方式(就像是管道中的水流一样)传输数据,实现TCP网络传输方案。
②数据报套接字(SOCK_DGRAM):以数据报形式(就像是用瓶子打包好的水一样)传输数据,实现UDP网络传输方案。
TCP套接字编程
服务端流程
先来看一个流程图:
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI0gTMx81dsQWZ4lmZf1GLlpXazVmcvwFciV2dsQXYtJ3bm9CX9s2RkBnVHFmb1clWvB3MaVnRtp1XlBXe0xCMy81dvRWYoNHLwEzX5xCMx8FesU2cfdGLwMzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsYTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5CN5IjN1gjY5cTNyUWZ1gTOyYzXwMTN1QTMyAzLcZDMyIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjLyM3Lc9CX6MHc0RHaiojIsJye.png)
代码实现
- 创建套接字
sockfd=socket.socket(socket_family=AF_INET,socket_type=SOCK_STREAM,proto=0)
功能:创建套接字对象
参数:
socket_family 网络地址类型:AF_INET表示ipv4
socket_type 套接字类型:SOCK_STREAM 流式;SOCK_DGRAM 数据报
proto(在应用层网络编程中用不到) 选择子协议:通常为0
返回值: 套接字对象
备注:底层/系统层套接字也针对某种协议,这些协议,有的时候会产生一些分支,也就是子协议。那么为啥在应用层网络编程中用不到proto这个参数呢?因为TCP协议和UDP协议没有子协议,所以参数设为0,即不选择任何子协议。
- 绑定地址
sockfd.bind(addr)
功能: 绑定本机网络地址(如果不写本机网络地址,则会报错)
参数: 二元元组 (ip,port) 比如('0.0.0.0',8888)
备注:addr为一个元组,这个元组有两个元素一个是网络地址(IP),一个是端口号(port)。
- 设置监听
sockfd.listen(n)
功能 : 将套接字设置为监听套接字,确定监听队列大小
参数 : 监听队列大小
备注1:并不是所有的套接字都具备监听的功能,即被客户端连接的功能。通过调用listen,我们的套接字对象才能被客户端连接。
备注2:一个服务端套接字对象可以同时连接多个客户端,与客户端进行连接需要进行3次握手,且连接的过程需要一个一个来进行。比如说,同时有5个客户端发起了连接请求,这时我们就会形成一个”先来后到”的队列,对这5个客户端的连接请求,一个一个进行处理。比如说,我们设置监听队列大小为5,表示队列最多容纳5个客户端等待处理。但有10个客户端同时发起了连接请求,这时,服务端会先处理第1个发起请求的那个客户端,前2~6个客户端则在等待队列中,最晚发起请求的4个客户端,则会被拒绝。
- 等待处理客户端连接请求
connfd,addr = sockfd.accept()
功能: 阻塞等待处理客户端请求(等待客户端连接,啥时候有客户端连接,才结束阻塞)
返回值:
connfd : 客户端连接套接字.
客户端连接后,新产生的专门与客户端进行通信的套接字对象。
则每当有一个客户端连接时,都会有一个新的专用于连接的套接字对象产生。
就像对每个客户端提供一个专属管家一样,提升用户端体验。
我们如果找到一个连接套接字,就可以知道它对应的客户端是谁
addr :连接的客户端地址
阻塞函数:当程序运行到这个函数时,则程序暂停执行。程序在等待某种条件,当条件满足后才继续执行,如:input()、sleep()。阻塞函数在IO操作中很常见。
- 消息收发
data = connfd.recv(buffersize)
功能 : 接受客户端消息
参数 :每次最多接收消息的大小(最多一次接受多少字节的消息)
返回值: 接收到的内容
n = connfd.send(data)
功能 : 发送消息
参数 :要发送的内容-bytes格式(字节串格式)
返回值: 发送的字节数
备注1:所有和网络相关的消息传送都得是bytes格式(字节串格式).发送和接收都得是字节串。
备注2:我们发送/接收的是字节串,但平时写/读的是字符串,所以我们需要用encode()/dencode()进行转换。
- 关闭套接字
sockfd.close()
功能:关闭套接字
举个例子
服务端代码:
import socket
#创建流式套接字
sockfd = socket.socket(socket.AF_INET, \
socket.SOCK_STREAM)
#绑定地址
sockfd.bind(('127.0.0.1', 8888))
#设置监听
sockfd.listen(5)
#等待处理客户端链接
print("Waiting for connect....")
connfd, addr = sockfd.accept()
#收发消息
data = connfd.recv(1024)
print("接收到的消息:", data.decode())
n = connfd.send(b'Receive your message')
print("发送了 %d 个字节数据" % n)
#关闭套接字
connfd.close()
sockfd.close()
我们运行一下,得到以下结果:
结果说明,程序阻塞在accept,等待客户端连接。所以这时,我们就要找一个客户端与之连接,下一节,我们就学一下客户端流程。
客户端流程
先看看流程图:
代码实现
-
创建套接字(和客户端代码相同)
注意:只有相同类型的套接字才能进行通信
- 请求连接
sockfd.connect(server_addr)
功能:连接服务器
参数:元组 服务器地址
-
收发消息(和客户端代码相同)
注意: 为了防止两端都阻塞,故recv和send要配合使用。比如,服务端是先send后recv,那么客户端则需要先recv后send。否则,若两端同时recv,则两端都会阻塞。
- 关闭套接字(和客户端代码相同)
举个例子
客户端代码:
服务端代码:
我们想要运行,但是若先运行客户端,则会报错。所以我们要先启动服务端
①运行服务端
查看服务端的控制台Console 2/A输出的结果:
②运行客户端
我们先看一下服务端的控制台Console 2/A输出的结果:
我们再看一下客户端的控制台Console 3/A输出的结果:
消息收发成功了,很好~
TCP套接字数据传输特点
①TCP连接中,当一端退出,另一端如果阻塞在recv,此时recv会立即返回一个空字串。
②TCP连接中,如果一端已经不存在,仍然试图通过send发送信息,则会产生BrokenPipeError
③一个监听套接字可以同时连接多个客户端,也能够重复被连接。
做个练习
要求1:一个客户端退出了,服务器不会退出,而是连接下一个客户端
要求2:客户端可以不停的循环发送消息
服务端代码:
#-*- coding: utf-8 -*-
import socket
#创建流式套接字
sockfd = socket.socket(socket.AF_INET, \
socket.SOCK_STREAM)
#绑定地址
sockfd.bind(('127.0.0.1', 8888))
#设置监听
sockfd.listen(5)
#等待处理客户端链接
while True:
print("Waiting for connect....")
try:
connfd, addr = sockfd.accept()
print("Connect from:", addr)
except KeyboardInterrupt:
print("退出服务")
break
# 收发消息
while True:
data = connfd.recv(1024)
# 得到空则退出循环
if not data:
break
print("接收到的消息:", data.decode())
n = connfd.send(b'Receive your message')
print("发送了 %d 个字节数据" % n)
connfd.close()
#关闭套接字
sockfd.close()
客户端代码:
#-*- coding: utf-8 -*-
from socket import *
#创建tcp套接字
sockfd = socket()
#发起连接
server_addr = ('127.0.0.1',8888)
sockfd.connect(server_addr)
#收发消息
while True:
data = input("消息:")
if not data:
break
sockfd.send(data.encode())
data = sockfd.recv(1024)
print("From server:",data.decode())
#关闭
sockfd.close()
先运行服务端,再运行客户端并发送消息。
客户端结果:
服务端结果:
网络收发缓冲区
①网络缓冲区有效的协调了消息的收发速度、缓解收发压力。
②send和recv实际是向缓冲区发送接收消息,当缓冲区不为空recv就不会阻塞。
网络缓冲区完成消息传递示意图:
举个例子
还记得我们上面那个练习么?其中一段代码是:
它表示,每次最多可接收消息大小为1024个字节.
我们来更改一下最大可接受的消息字节数为5:
用客户端发送消息:
服务端结果:
客户端结果:
则说明,客户端1次发送,服务端分3次接收了,同时返回给服务端3句’ Receive your message’也就是说,服务端的内层循环(如下图所示)循环了3次
备注1:所以我们一开始说【函数recv()是阻塞函数,当发送方不发消息,就会阻塞】。但其实更准确的来说,并不是发送方不发消息就会阻塞,接收方其实是先从缓冲区去拿消息,当缓冲区为空时,会阻塞,当缓冲区一直有消息时,则会一直获取消息,一直不阻塞。
注意! 运行上面的代码时,客户端有时候也会出现以下这种情况:
TCP粘包
-
原因
TCP以字节流方式传输,没有消息边界。多次发送的消息被一次接收,此时就会形成粘包。
-
影响(分情况,比如:传一部电影/发送用户名消息)
①如果发送的内容中,每个信息都有独立的含义(比如:发送几个用户姓名),需要接收端独立解析,此时,这时粘包会有影响。
②如果发送的是一个字节流文件,是一个连在一起的整体(比如:发送一部电影),接收端无需单独解析,而是将所有接收到内容最终合成一个整体,这时粘包没啥影响。
-
处理方法
①人为的添加消息边界,比如:每次发送一个姓名时,在姓名后加一个特殊符号。
②控制发送速度(因为粘包的产生是因为收发速度不协调),比如用sleep()函数调节。