TCP简单实现
参考文档: https://www.cnblogs.com/linhaifeng/articles/6129246.html
# socket层
Socket是应用层与TCP/IP协议族通信的中间软件抽象层, 它是一组接口.
# 简单介绍
socket就是一个套接字软件程序. 是对传输层及以下的封装.
网络编程,网络包含两方面物理连接介质和协议..我们只需关注协议即可..
我们无需深入理解tcp/udp协议, 套接字位于应用层和传输层之间,将传输层以下的协议都封装成了接口!!!
我们只需要遵循socket的规定去编程, 写出的程序自然就是遵循tcp/udp标准的!
也就是说,它给应用层应用程序的开发提供了传输层及以下的一个个接口..
[扫盲篇] -- 了解即可
一开始,套接字被设计用在同一台主机上多个应用程序之间的通讯.这也被称进程间通讯,或IPC.
套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的.
△ 基于文件类型的套接字家族 名字:AF_UNIX
两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信
△ 基于网络类型的套接字家族 名字:AF_INET
python支持很多种地址家族,由于我们只关心网络编程,所以大部分时候我么只使用AF_INET!
2
3
4
5
6
7
8
9
# 工作流程
以下是套接字实现的TCP协议的流程!
"★ 服务端套接字函数"
socket() -- 返回套接字对象
bind() -- S端绑定IP和端口
listen() -- 监听C端的连接请求
accept() -- 阻塞等待C端的连接请求,接收后三次握手建立成功
"★ 客户端套接字函数"
socket() -- 返回套接字对象
connect() -- C端发送连接请求
"★ 通信循环"
read()
write()
2
3
4
5
6
7
8
9
10
11
12
13
server必须遵循:
1> 稳定运行, 对外一致提供服务!
2> 服务必须绑定固定的IP和端口!
Ps: url是在ip和port之上标识唯一的资源的.
# 简单的socket程序
先启动服务端,再启动客户端
# 三报文连接建立
客户端的connect对应服务端的accept !!!
☆ 注意哦!
S端有两种套接字对象.phone和conn.
phone调用bind、listen为accept做准备,是用于建立TCP连接的!
conn是建立连接的成果.用于传输数据的!
C端只有一种套接字对象.phone.
Q: 半连接池backlog 限制的是<同一时刻>的请求数!如何理解?
A: 服务端可以服务于多个客户端.
C端向S端发送的连接请求,要先从半连接池里走一遭,建立好TCP连接后,该连接会进入全连接池.
(该过程特别快.)
CPU会以时间片的形式在全连接池里的任务/连接之间来回切换..
即服务端“并发”的服务这些已经建立好连接的客户端.
受限于cpu性能和内存大小,全连接池里的连接数量有限(即并发能力有限).
高并发的情况下.客户端的请求连接会长时间停在半连接池里..现实中的体现就是网页一直在加载.
半连接池满了,往后的连接请求直接连接不上,超时..
(当然半连接池跟全连接池一样占用的是内存空间,设置的连接数量受限于内存的大小.)
# 服务端.py
import socket
"""
socket.socket() 调用socket模块的socket类得到一个套接字对象
其参数为:
套接字的种类 socket.AF_INET # -- 不传参默认为网络类型
套接字的协议 socket.SOCK_STREAM # -- 不传参默认为流式协议,即TCP协议
socket.SOCK_DGRAM # -- 数据报协议,即UDP协议
"""
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # -- 买诺基亚手机
# <socket.socket fd=3, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('0.0.0.0', 0)>
print(phone)
"""
用(元祖) 绑定IP+端口 127.0.0.1本地回环地址用于开发测试(c/s端 运行在同一台机器上)
"""
phone.bind(("127.0.0.1", 8080)) # -- 插入/绑定手机卡
"""
半连接池backlog 限制的是<同一时刻>的请求数!
Ps:意思就是接收的客户端连接都要从半连接池队列里走一遭.Hhh
"""
phone.listen(5) # -- 开机,'监听连接请求'
"""
调用套接字对象phone的accept()方法,返回了一个元祖 -- (三次握手建立的双向连接,(客户端的IP,客户端的端口))
注意两点:
1> 服务端的accept对应客户端的connect操作 它们在底层完成了三次握手!
2> 通过三次握手形成了两条道
一条道客户端到服务端;一条道服务端到客户端.
conn对象既可以收消息也可以发消息,都应该是bytes格式
"""
print("start wait..")
conn, client_addr = phone.accept() # -- 阻塞等待,接收电话连接
# <socket.socket fd=4, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 49978)>
# -- fd文件描述符,对应操作系统中打开文件的编号;family网络套接字;type流式协议;
# laddr本地的"此处是服务端",raddr远程的"这里是客户端".
print(conn)
# ('127.0.0.1', 49978) -- 每次连接的客户端端口号都是不一样的,是客户端的操作系统随机分配的
print(client_addr)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 客户端.py
import socket
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# <socket.socket fd=3, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('0.0.0.0', 0)>
print(phone)
phone.connect(('127.0.0.1', 8080)) # -- 指定服务端的IP和端口
# <socket.socket fd=3, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 49978), raddr=('127.0.0.1', 8080)>
# -- laddr本地的"此处是客户端",raddr远程的"这里是服务端".
print(phone)
2
3
4
5
6
7
8
9
# 收发消息/阻塞之处
收发的得是基于网络传输的二进制数据!! bytes类型的数据.
"""
★ -- 服务端
会阻塞的地方有两个: phone.accept() conn.recv(1024)
"""
import socket
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.bind(("127.0.0.1", 8080))
phone.listen(5)
conn, client_addr = phone.accept()
# -- 通信: 收、发消息
data = conn.recv(1024) # -- 最大接收的字节数
print('来自客户端的数据:', data) # 来自客户端的数据: b'hello'
conn.send(data.upper()) # -- 回数据
# -- 关闭
conn.close()
phone.close()
"""
★ -- 客户端
会阻塞的地方只有一个: phone.recv(1024)
"""
import socket
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.connect(('127.0.0.1', 8080))
# -- 通信: 发、收消息
# phone.send(bytes('hello', encoding='utf-8'))
phone.send('hello'.encode('utf-8')) # -- 基于网络传输得是二进制的数据 bytes类型的数据
data = phone.recv(1024)
print('来自服务端的数据:', data) # 来自服务端的数据: b'HELLO'
# -- 关闭
phone.close()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 服务端重启端口报错
报错信息: OSError: [Errno 48] Address already in use
这个是由于服务端仍然存在四次挥手的time_wait状态在占用地址!!!
Ps: 1.tcp三次握手,四次挥手 2.syn洪水攻击 3.服务器高并发情况下会有大量的time_wait状态
One_Piece@DCdeMacBook-Air ~ % netstat -an | grep 8080
tcp4 0 0 127.0.0.1.8080 127.0.0.1.49978 TIME_WAIT
2
1> 改端口号, 服务端bin和客户端connect里的端口号改的一致
2> 设置socket通信的配置项 <解决端口占用的问题> 设置后正在回收的话重用,重用ip和端口
phone=socket(AF_INET,SOCK_STREAM)
# -- 启动服务端再关闭操作系统回收IP和端口需要时间, 再次启动就会出现冲突的问题
# -- REUSEADDR重复使用地址
phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) # -- 就是它,在bind前加
phone.bind(('127.0.0.1',8080))
3> 发现系统存在大量TIME_WAIT状态的连接, 通过调整linux内核参数解决
详见连接: https://www.cnblogs.com/linhaifeng/articles/6129246.html#_label7
# 加上通信循环
# 收发消息进行循环
"""
★ -- 服务端
"""
import socket
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.bind(('127.0.0.1', 8080))
phone.listen(5)
conn, client_addr = phone.accept()
while True: # -- 通信循环
data = conn.recv(1024)
print('来自客户端的数据:', data)
conn.send(data.upper())
conn.close()
phone.close()
"""
★ -- 客户端
"""
import socket
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.connect(('127.0.0.1', 8080))
while True: # -- 通信循环
msg = input('>>: ').strip()
phone.send(msg.encode('utf8'))
data = phone.recv(1024)
print('来自服务端的数据:', data)
phone.close()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 查看状态
"""
★ -- 先关闭客户端,再关闭服务端
"""
# -- 启动服务端
One_Piece@DCdeMacBook-Air ~ % netstat -an | grep 8080
tcp4 0 0 127.0.0.1.8080 *.* LISTEN
# -- 启动客户端
One_Piece@DCdeMacBook-Air ~ % netstat -an | grep 8080
tcp4 0 0 127.0.0.1.8080 127.0.0.1.51398 ESTABLISHED
tcp4 0 0 127.0.0.1.51398 127.0.0.1.8080 ESTABLISHED
tcp4 0 0 127.0.0.1.8080 *.* LISTEN
# -- 先在控制台强行关闭客户端
One_Piece@DCdeMacBook-Air ~ % netstat -an | grep 8080
tcp4 0 0 127.0.0.1.8080 127.0.0.1.51398 CLOSE_WAIT # 服务端
tcp4 0 0 127.0.0.1.51398 127.0.0.1.8080 FIN_WAIT_2 # 客户端
tcp4 0 0 127.0.0.1.8080 *.* LISTEN
# -- 在控制台强行关闭服务端
tcp4 0 0 127.0.0.1.51398 127.0.0.1.8080 TIME_WAIT # 客户端
"""
★ -- 先关闭服务端,再关闭客户端
"""
# -- 启动服务端
One_Piece@DCdeMacBook-Air ~ % netstat -an | grep 8080
tcp4 0 0 127.0.0.1.8080 *.* LISTEN
# -- 启动客户端
One_Piece@DCdeMacBook-Air ~ % netstat -an | grep 8080
tcp4 0 0 127.0.0.1.8080 127.0.0.1.51518 ESTABLISHED
tcp4 0 0 127.0.0.1.51518 127.0.0.1.8080 ESTABLISHED
tcp4 0 0 127.0.0.1.8080 *.* LISTEN
# -- 先在控制台强行关闭服务端
One_Piece@DCdeMacBook-Air ~ % netstat -an | grep 8080
tcp4 0 0 127.0.0.1.8080 127.0.0.1.51518 FIN_WAIT_2 # 服务端
tcp4 0 0 127.0.0.1.51518 127.0.0.1.8080 CLOSE_WAIT # 客户端
# -- 在控制台强行关闭客户端
One_Piece@DCdeMacBook-Air ~ % netstat -an | grep 8080
tcp4 0 0 127.0.0.1.8080 127.0.0.1.51518 TIME_WAIT # 服务端
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 客户端断开连接BUG
在进行查看状态的实验中, 我们单方面的先断开客户端, 发现服务端会一直打印
来自客户端的数据: b''
至于为何会循环打印 b''
.. 别问,问就是linux系统的机制,不晓得..╮( ̄▽ ̄"")╭
解决方案: 服务端进行如下修改.
我们说conn是三次握手建立双向通道的产物 客户端单方面挂掉了. conn.recv(1024)
会一直阻塞.
1> 服务端linux系统下会异常,一直接受空字符串‘死循环’,cpu的占用率会很高(top命令查看).
2> 服务端windows上会直接报错.
"""
★ -- 服务端
"""
import socket
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.bind(('127.0.0.1', 8080))
phone.listen(5)
conn, client_addr = phone.accept()
while True: # -- 通信循环
try:
data = conn.recv(1024)
if not data: break # -- 适用于linux操作系统
# 在客户端单方面断开连接,服务端才会出现收空数据的情况!!!
print('客户端的数据:', data)
conn.send(data.upper())
except ConnectionResetError: # -- 适用于windows操作系统
break
conn.close()
phone.close()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 半连接池的效果
现在有1个实现了"通信循环"的服务端A, 6个实现了"通信循环"的客户端b、c、d、e、f、g...
先启动A,再启动b,b成功与A建立连接..
此时A的代码停留在while True通信循环里;b的代码也运行到了通信循环处!
启动c、d、e、f、g, 发现它们都没有成功与A成功建立连接
直接体现在: 输入"hello",没有来自服务端A的反馈.. c、d、e、f、g此时正处于A的半连接池里..
要搭理c、d、e...它们的请求连接需要执行 A中的 phone.accept()
代码..
可现在A的代码运行停留在通信循环处!!
若这时还有个客户端h试图与服务端A建立连接.. 会直接报错连接超时: TimeoutError
因为服务端A设置的半连接池个数为5 phone.listen(5)
Ps: 截图中我们设置的客户端的半连接池大小为1.. 设置大了就展示不完整啦...Hhhh
即b成功连接,c在半连接池里,d直接连接失败..
# 加上连接循环
前面"半连接池的效果"的实验中, 因为服务端只有通信循环,服务端一旦通过
phone.accept()
与某个客户端建立连接后,就不会与其它客户端建立连接了,因为服务端的代码从上到下执行,建立连接后,客户端的代码会停留在了while True的通信循环里... 无法从半连接池里取其它客户端的连接请求!!客户端关闭后,服务端会处于TIME_WAIT状态,不再往外提供服务.
# 连接通信双循环
不能因为客户端的终止影响服务端一直往外稳定提供服务!!!
小声BB: 前面的BUG解决的是稳定问题,连接循环解决的是一直提供服务的问题..
import socket
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
phone.bind(('127.0.0.1', 8080))
phone.listen(5)
while True: # -- 连接循环(则中的方案)
conn, client_addr = phone.accept()
while True: # -- 通信循环
try:
data = conn.recv(1024)
if not data: break
print('来自客户端的数据:', data)
conn.send(data.upper())
except ConnectionResetError:
break
conn.close()
phone.close()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
分析该服务端代码的流程:
1> 启动服务端拿到套接字对象, 绑定、监听;
2> 进入连接循环,接收一个客户端套接字对象的connect()连接请求,双方建立TCP连接!
3> 服务端基于这个连接进行通信循环;
4> 直到此次通信结束,conn.close()关闭此次连接..
服务端套接字再从半连接池里取一个客户端的连接请求,建立双方的连接.. 如此往复.
# 现目前的局限
服务端应该满足两点, 1> 一直往外 稳定 提供服务 ; 2> 并发的提供服务!即同时服务多个客户端.
但现目前的代码,服务端建连接和通信是一步一步来的.只能实现第一点!
通信的时候不能再次建立连接.只有此次通信结束,再建立另外一个连接..
要想实现第二点,并发的提供服务,需要多线程多进程的知识加以优化解决.. 后续网络并发编程会详细阐述!!!
怎么个并发法呢?
打个比方,服务端就是个饭店,门口有个小二,一直不停的招揽客人(不停的从半连接池里取连接),客人进店后(TCP连接建立成功),饭店会招一个服务员一对一的服务这名客人(干通信循环的活).. 同理,小二招揽的第二个客人进来后,饭店会再招一个服务员一对一的服务该客人..
# socket通信底层原理
# 客户端发空BUG
前面的客户端程序还有个bug!! 客户端phone.send(b'')发送空字符后,会阻塞住,为什么呢?
先找问题出现在哪里!! 在客户端里使用print打印大法..Hhh
"""
★ -- 服务端
"""
import socket
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
phone.bind(('127.0.0.1', 8080))
phone.listen(5)
while True:
conn, client_addr = phone.accept()
while True:
try:
# -- 正常情况下,客户端发送过来的数据大小必须大于0bytes才会被服务端接收!!
# 客户端接收数据同理!
data = conn.recv(1024)
if not data: break # -- 在客户端单方面断开连接,出现异常,服务端才会出现收空数据的情况!!!
print('来自客户端的数据:', data)
conn.send(data.upper())
except ConnectionResetError:
break
conn.close()
phone.close()
"""
★ -- 客户端
"""
import socket
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.connect(('127.0.0.1', 8080))
while True:
msg = input('>>: ').strip() # -- 直接回车,输入空字符串
phone.send(msg.encode('utf8'))
print("has send ...") # -- 会打印
data = phone.recv(1024)
print("has recv ...") # -- 不会打印
print('来自服务端的数据:', data)
phone.close()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
说明,客户端没有recv()收到服务端发送过来的消息!! 大胆推测是服务端没有发消息给客户端!!!
继续分析, <正常情况下> 服务端永远都收不到"空"的数据!!
通过前面的实验,我们知道服务端收到空的数据代表出现异常了,客户端单方面断开连接服务端才会收到空!!
所以<正常情况下>客户端发送的 b""
空数据, 服务端是收不到的, 服务端就会一直阻塞在 conn.recv(1024)
此处, 傻不拉几的等待客户端发消息过来,也就不会发消息给客户端!!
简单来说, 正常情况下 ,客户端发送的 b""
数据,服务端接收不到, 服务端只会 “接收到” 大于0bytes的数据!!
!! 看样子是服务端没发数据,实则该问题bug的触发者/根源在于客户端...Hhh
解决方案: send之前加个判断,杜绝客户端发空数据
"""
★ -- 客户端
"""
import socket
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.connect(('127.0.0.1', 8080))
while True:
msg = input('>>: ').strip()
if not msg: continue # -- 判断
phone.send(msg.encode('utf8'))
data = phone.recv(1024)
print('来自服务端的数据:', data)
phone.close()
2
3
4
5
6
7
8
9
10
11
12
13
14
# 底层原理
先明白两点:
msg数据是存在于应用程序自个儿的内存中的!
客户端软件需调用 '硬件'网卡将msg数据沿着网线送到服务端,但应用程序/客户端不能直接操作硬件..
1> 客户端执行 send()方法 将< 自己这个应用程序内存 里的msg数据> 拷贝给 <操作系统 缓存 "cache?">
由客户端操作系统基于TCP协议调网卡通过网络传输数据给服务端.
服务端的OS收到会基于TCP协议回一个信息,客户端OS里的msg数据就可以清除掉啦!
2> 服务端的recv()会从服务端的操作系统缓存中剪切/直接拿从客户端传送过来的msg数据.
准确点来说,服务端软件是一个进程,OS软件也是一个进程,OS是拷贝数据给客户端,拷贝后OS会自动将拷贝的这部分数据销毁.. 所以看起来是直接拿的.
应用程序里的数据(客户端产生的/服务端接收的 msg数据),若没有绑定任何的变量名,gc机制会自动回收!
注意: 基于TCP协议,客户端操作系统缓存中的msg数据要等服务端回复相应的确认号后才会清除..所以TCP可靠.
特别强调! 不是一发对应一收,发收都跟对方不沾边!发是发给自己的操作系统,收也是从自己的操作系统中收!
Ps: UDP发收也是与OS交互,但与TCP不同的一点在于,UDP不可靠,OS将数据发送了,OS缓存中的该数据就直接删除了..可不会管该数据可不可达..
再次分析, "客户端发空BUG".. 客户端send拷贝一个空的bytes字符串给客户端的操作系统内存..
客户端OS直接原地问号..有东西吗?没有,压根不会进行任何网络传输的操作..
服务端一直苦苦等待服务端操作系统内存接收到的数据.. 服务端纯纯大怨种Hhhh