Socket原理-套接字

# 客户端/服务器架构
1.硬件C/S架构(打印机)
2.软件C/S架构(web服务)
3.B/S架构是C/S架构的一种

socket是为了完成C/S架构的开发
安装socket写代码,socket在接口内部,会自动遵循TCP/IP协议
ip+mac定位到主机,port定位到指定的应用程序
port(0~65535) 1024是系统用的 之后自己随便用
pid是同一台机器上不同进程或现代的标识,而一个程序有很多进程

# 套接字

套接字
同台电脑中,两个进程之间不能直接通信,需要套接字调用文件系统或者基于网络来间接实现两者的通信
AF_INET :一个客户端进程和一个服务端进程基于网络实现通信
6af190aa2e9656eaacf0fe19c857acf7.png  

1. 服务端
```python
import socket
phone=socket.socket(socket.AF_INET, socket.SOCK_STREAM)#创建socket进程
#套接字对象创建完成!
#socket.AF_INET代表基于网络的套接字 socket.SOCK_STREAM代表基于TCP协议
phone.bind(('127.0.0.1',8000))#绑定自己的唯一标示信息
phone.listen(5)#最多跟5个客户端通信
print("我这里没卡住")
conn,addr=phone.accept() #元组 得到连接和地址信息,拿到TCP链接,等客户端消息也是它

msg=conn.recv(1024) #长度
print('客户端发来的消息是: ',msg)
conn.send(msg.upper())#发消息

conn.close()#关闭连接
phone.close()#关socket进程
```

2. 客户端

```python
import socket

phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
#创建socket进程
phone.connect(('127.0.0.1',8000)) #指定连接
phone.send('hello'.encode('utf-8')) #主动发消息
#收发消息要基于二进制,把内容编码成二进制bytes
data=phone.recv(1024)#接收服务端的回应
print('收到服务端的发来的消息:',data)
```
注意事项:
1.主程序不要叫“socket.py”或系统有的名字,否则import导入错误
2.mac中常见的程序重启后的错误
1b2237a42cf2e977bf3f99aee9356ac6.png  
原因:程序关掉后,监听的端口没有立即清楚掉,再次运行,报错

底层实现:
tcp协议在传输层
7253405bf587a5093287254a86000b0d.png  

查看linux端口状态: netstat -an |grep 8080

tcp为何叫安全链接:
1. 双向通道
2. 确认机制
**建立连接三次握手 :**

1.客户端请求建立客户端-服务端的连接
2.服务端同意建立,客户端状态变为ESTABLISHE,并请求建立服务端-客户端的连接
3.客户端同意建立连接,客户端状态变为ESTABLISHE

SYN洪水攻击:无数个客户端一直发syn请求,服务端好人肯定会回应,并等着客户端的回应,客户端不再回应,进入服务端的"半链接池"backlog,即 listen(n)为最大挂起数量,正常的访问就进不来了
解决办法:减少尝试等待客户端回应次数,listen值调大

**tcp数据传输:**
客户端发消息-服务端读-服务端发确认收到消息,服务端发消息-客户端读-客户端发确认收到消息

**tcp断开连接四次挥手:conn.close()**
谁先发完包,就是谁主动发起断开请求
比如客户端包发完了,准备断开客户端到服务端的连接
这时客户端进入FIN_WAIT_1状态,服务端发送ACK给客户端,客户端收到后马上断开客户端到服务端的连接,并进入FIN_WAIT_2状态,接着服务端又发一条FIN,客户端收到进入TIME_WAIT状态,客户端回ACK,服务端收到后,断开服务器-客户端连接

**谁先断开连接**
都有可能,谁数据发完了,就是谁发起的断开请求,还没发完的那个,肯定不会发起断开请求,可以观察,只要谁有FIN_WAIT,TIME_WAIT状态,谁就是发起断开连接的一方

而生产状态的大并发的服务器,只要服务器发完包,就发起断开连接,节省资源

**为何需要3次握手,4次挥手**
* 3次握手中服务端同意建立,和请求建立可以放一起发,这时候没有数据传输,只是单纯想互相建立连接
* 4次挥手中,客户端说发完了要断开连接,服务端可能还要数据没发完呢,不能把同意断开连接和请求断开连接一起发,否则丢包

**linux服务端重启间隔较短情况下,可能报错:地址正在使用 停留在TIME__WAIT状态**
e58b5cb80ca1e5c8feb5abf8c660fbd0.png  
**出现原因**
服务端在conn.close()关闭连接后,很短时间就关闭了整个socket进程,这时端口可能遗留着TIME__WAIT或者FIN_WAIT2状态在占用地址,4次挥手还没完成。此时重启服务 没办法再监听原来的地址
端口状态在等它上次发出的ACK信息准确无误的到达被断开连接方(确保最后的ACK包有充足的时间让对方接受到)

**处理方法**
方法一:在服务端的socket对象.bind(("127.0.0.1",8080))上面加一条
socket对象.setsocketopt(SQL_SOCKET,SO_REUSEADDR,1) #重新使用那个未解绑的ip地址
方法二:修改linux内核,修改端口保留FIN_WAIT状态的超时时间(TIMEOUT时间)
```
发现系统存在大量TIME_WAIT状态的连接,通过调整linux内核参数解决,
vi /etc/sysctl.conf
编辑文件,加入以下内容:
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_fin_timeout = 30

然后执行 /sbin/sysctl -p 让参数生效。

net.ipv4.tcp_syncookies = 1 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭;

net.ipv4.tcp_tw_reuse = 1 表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;

net.ipv4.tcp_tw_recycle = 1 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。

net.ipv4.tcp_fin_timeout 修改系統默认的 TIMEOUT 时间
```

# socket收发消息原理剖析
把后期经常改动的变量提取出来
服务端:
```python
# import socket
# 一般不提倡import*,socket比较特殊
#都导入进来后就不需要每次socket.来调用方法了
from socket import *
ip_port=('127.0.0.1',8083)
back_log=5
buffer_size=1024

tcp_server=socket(AF_INET,SOCK_STREAM)
tcp_server.bind(ip_port)
tcp_server.listen(back_log)

while True:
#循环的等待接收服务
print('服务端开始运行了')
conn,addr=tcp_server.accept() #服务端阻塞等待链接
print('双向链接是',conn) #链接信息
print('客户端地址',addr) #地址+端口

while True:
#循环的为这个链接服务
try:
data=conn.recv(buffer_size)
print('客户端发来的消息是',data.decode('utf-8'))
conn.send(data.upper())
except Exception:
break
conn.close()

tcp_server.close()
```
客户端
```python
# import socket
from socket import *
ip_port=('127.0.0.1',8083)
back_log=5
buffer_size=1024

tcp_client=socket(AF_INET,SOCK_STREAM)
tcp_client.connect(ip_port)

while True:
msg=input('>>: ').strip()
if not msg:continue #如果客户端输入为空,重新回到输入
tcp_client.send(msg.encode('utf-8'))
print('客户端已经发送消息')
data=tcp_client.recv(buffer_size)
print('收到服务端发来的消息',data.decode('utf-8'))

tcp_client.close()

# recv(1024),其实是在内核态内存收消息
# send(msg),其实是消息从用户态内存拷贝到内核态内存
```
c9b2ec06f809809c99435d31b4baad0e.png  
如果客户端直接回车,即发了一个None,客户端send空到客户端内核态内存
而服务端的内核态缓存,没东西。,自然卡住了,后面的send就不会执行。客户端在send空后,会一直等着收内核态缓存的信息,而客户端没收到新消息,所以也卡住了

卡住recv 是由于:内核态缓存没东西,跟对方没关系

recv谁发起的:由用户态内存中的应用程序发起的,可以是客户端,可以使服务端
最多收到设定的字节数

### 服务端循环链接请求+循环收发消息

堆结构:先进先出,吃了拉 先吃进去的先拉出来 python中的堆结构为队列
栈结构:先进后出,后进先出 吃了吐,后面吃的先吐出来 例如queue.LifoQueue

客户端直接终止:四次挥手都没有,直接中断的,而服务端本来等着收,conn突然被干掉了,recv就没意义了(远程主机强迫关闭了一个现有连接)

C/S架构的美好愿望,服务端启动后,永远运行下去,服务很多人

**循环提供服务**
如果服务端正在为客户端a提供服务,那么服务端进入 “为a链接服务”的循环中,此时这个循环还没结束,为a链接服务的conn.close()没有执行,回不到上一个循环,即无法接收新的会话连接
此时客户端b试图建立链接,send到自己的内核态内存,并发往服务器,进入服务器的block_log连接池中挂起。 等对a的服务进程完成断开后,进入下一个接收连接循环,服务端的内核态内存才会把挂起的“b连接”发给用户态内存,服务端程序才能接收并进入服务循环

客户端b试图想建立链接,这时候客户端如果发了一条消息,其实发消息就已经进入客户端的循环,只是服务端没有回应,客户端的内核态内存没新数据,故客户端的tcp_client.recv(buffer_size)没有收到信息,自然前面的data= 就没有被赋值,再次就中断等待了

此时手动结束客户端a进程,服务端conn被干掉,报错:远程主机强迫关闭了一个现有连接,服务端进程卡死。故需要在 为a连接服务的 循环,加一个异常处理 ,遇到问题 break,继续运行退出循环

```
避免通信循环中的报错
1.服务端在conn.recv()下一句
用if 为空 :break 解决conn断开,服务端一直收空消息造成的死循环
2.服务端在通信循环 用处理客户端进程非正常中断造成的报错(远程主机强迫关闭了一个现有的连接)
try:
#通信循环
except Exception as e:
print(e)
break
```

服务端:
```python
import socket
"""
服务端
固定的ip和port
24小时不间断提供服务
"""
server = socket.socket() # 生成一个对象
server.bind(('127.0.0.1',8080)) # 绑定ip和port
server.listen(5) # 半连接池

while True:
conn, addr = server.accept() # 等到别人来 conn就类似于是双向通道
print(addr) # ('127.0.0.1', 51323) 客户端的地址
while True:
try:
data = conn.recv(1024)
print(data) # b'' 针对mac与linux 客户端异常退出之后 服务端不会报错 只会一直收b''
if len(data) == 0:break
conn.send(data.upper())
except ConnectionResetError as e:
print(e)
break
conn.close()
```
客户端
```python
import socket

client = socket.socket()
client.connect(('127.0.0.1',8080))

while True:
msg = input('>>>:').encode('utf-8')
if len(msg) == 0:continue
client.send(msg)
data = client.recv(1024)
print(data)
```

**TCP服务端防止收空**
linux、unix遇到客户端终端,服务端不会抛出异常,而是recv会一直接收None,跟windows 不一样
解决方法:
```python
while True:
#循环的为这个链接服务
data=conn.recv(buffer_size)
if not data:break #如果收到的是空,退出循环
print('客户端发来的消息是',data.decode('utf-8'))
conn.send(data.upper())
conn.close()
```


# 基于TCP的套接字
tcp服务端
```python
ss = socket() #创建服务器套接字
ss.bind() #把地址绑定到套接字
ss.listen() #监听链接
inf_loop: #服务器无限循环
cs = ss.accept() #接受客户端链接
comm_loop: #通讯循环
cs.recv()/cs.send() #对话(接收与发送)
cs.close() #关闭客户端套接字
ss.close() #关闭服务器套接字(可选)
```
tcp客户端
```python
cs = socket() # 创建客户套接字
cs.connect() # 尝试连接服务器
comm_loop: # 通讯循环
cs.send()/cs.recv() # 对话(发送/接收)
cs.close() # 关闭客户套接字
```
# 基于UDP的套接字
UDP服务端
```python

ss = socket() #创建一个服务器的套接字
ss.bind() #绑定服务器套接字
inf_loop: #服务器无限循环
cs = ss.recvfrom()/ss.sendto() # 对话(接收与发送)
ss.close() # 关闭服务器套接字
```
没有listen:listen是用来挂连接请求的,半链接池,UDP没有链接
少了一个链接循环:不需要链接

UDP客户端
```python
cs = socket() # 创建客户套接字
comm_loop: # 通讯循环
cs.sendto()/cs.recvfrom() # 对话(发送/接收)
cs.close() # 关闭客户套接字
```
没有绑定:直接创建套接字,不需要绑定
只需要通讯循环

示例:
UDP服务端
```python
from socket import *
ip_port=('127.0.0.1',8080)
buffer_size=1024

udp_server=socket(AF_INET,SOCK_DGRAM) #数据报
udp_server.bind(ip_port)

while True:
data,addr=udp_server.recvfrom(buffer_size)
#客户端发的是(二进制内容,ip_port),所以收到的是元组
print(data)

udp_server.sendto(data.upper(),addr)
```
UDP客户端
```python
from socket import *
ip_port=('127.0.0.1',8080) #服务端的
buffer_size=1024

udp_client=socket(AF_INET,SOCK_DGRAM) #数据报

while True:
msg=input('>>: ').strip()
udp_client.sendto(msg.encode('utf-8'),ip_port)
#没有链接,每次要指定发给哪个ip端口
#发的是(二进制内容,ip_port)的元组
data,addr=udp_client.recvfrom(buffer_size)
# 收到的也是(二进制内容,ip_port)的元组
# print(data.decode('utf-8'))
print(data)
```
# recv与recvfrom的区别及基于UDP实现NTP服务
tcp收和udp收,都是从自己的缓冲区拿内容,tcp收不到空内容,udp却可以收到(None,ip_port)元组
recv在自己的缓冲区为空时,阻塞
recvfrom在收到一个空内容时,虽然内容为空,但是带了ip_port的报文头

TCP 一端发完一个包后,收到对面的ACK确认收到响应,才会在内核缓冲区把数据清空
socket用户态内存发给内核态内存,本质是copy操作

UDP不依赖于服务端
UDP没有链接,天然实现会话的并发

 

**NTP服务**
NTP服务端
```python
from socket import *
import time
ip_port=('127.0.0.1',8080)
buffer_size=1024

udp_server=socket(AF_INET,SOCK_DGRAM) #数据报
udp_server.bind(ip_port)

while True:
data,addr=udp_server.recvfrom(buffer_size)
print(data)

if not data:
fmt='%Y-%m-%d %X'
else:
fmt=data.decode('utf-8')
back_time=time.strftime(fmt)
#获取自己的时间是个字符串,如果是数字类型,要先转字符串,再转二进制
udp_server.sendto(back_time.encode('utf-8'),addr)
```
NTP客户端
```python
from socket import *
ip_port=('127.0.0.1',8080)
buffer_size=1024

udp_client=socket(AF_INET,SOCK_DGRAM) #数据报

while True:
msg=input('>>: ').strip()
udp_client.sendto(msg.encode('utf-8'),ip_port)

data,addr=udp_client.recvfrom(buffer_size)
print('ntp服务器的标准时间是',data.decode('utf-8'))
```

 

posted @ 2019-08-14 18:57  坚持fighting  阅读(436)  评论(0编辑  收藏  举报