一、套接字的概念及分类
(一)套接字是什么
套接字是一种通信机制,凭借这种机制,客户/服务器系统的开发工作既可以在本地单机上进行,也可以跨网络进行,Linux所提供的功能(如打印服务,ftp等)通常都是通过套接字来进行通信的,套接字的创建和使用与管道是有区别的,因为套接字明确地将客户和服务器区分出来,套接字可以实现将多个客户连接到一个服务器。
套接字,也称为BSD套接字,是支持TCP/IP的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。应用层通过传输层进行数据通信时,TCP和UDP会遇到同时为多个应用程序进程提供并发服务的问题。
简单的举例说明:Socket=Ip address+ TCP/UDP + port。
(二)套接字的分类
1.基于文件类型
套接字家族的名字:AF_UNIX。
2.基于网络类型
套接字家族的名字:AF_INET。
二、套接字工作流程
(一)面向连接的套接字Socket通信工作流程
1.服务器先用socket函数来建立一个套接字,用这个套接字完成通信的监听;
2.用bind函数来绑定一个端口号和一个IP地址。因为本地计算机可能有多个网址和IP,每一个IP和端口有多个端口,需要指定一个IPhe端口进行监听。
3.服务器调用listen函数,使服务器的这个端口和IP处于监听状态,等待客户机的链接。
4.客户机用socket函数建立一个套接字,设定远程IP和端口。
5.客户机调用connect函数连接远程计算机指定的端口。
6.服务器用accept函数来接受远程计算机的连接,建立起与客户机之间的通信。
7.建立连接以后,客户机用write函数想socket中写入数据。也可以用read函数读取服务器发送来的数据。
8.服务器用read函数读取客户机发送来的数据,也可以用write函数来发送数据。
9.完成通信以后,用close函数关闭socket连接。
(二)面向无连接的套接字Socket通信工作流程
无连接的通信不需要建立起客户机与服务器之间的连接,因此在程序中没有简历连接的过程。进行通信之前,需要简历网络套接字。服务器需要绑定一个端口,在这个端口上监听收到的信息。客户机需要设置远程IP和端口,需要传递的信息需要发送到这个IP和端口上。
(三)socket() 模块函数用法
1.socket() 模块函数基本用法
import socket
socket.socket(socket_family, socket_type, protocal=0)
# socket_family 可以是 AF_UNIX 或 AF_INET;
# socket_type 可以是 SOCK_STREAM 或 SOCK_DGRAM;
# protocal 一般不填,默认值为0
# 获取tcp/ip套接字
tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 获取udp/ip套接字
udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 由于 socket 模块中有太多属性我们在这里破例使用了'from module import *'语句。使用 'from socket import *',我们就把 socket 模块里的所有属性都带到我们的命名空间里了,这样能 大幅减短我们的代码。
# 例如:tcpSock = socket(AF_INET, SOCK_STREAM)
2.服务端套接字函数
s.bind() 绑定(主机、端口号)到套接字
s.listen() 开始TCP监听
s.accept() 被动接受TCP客户的连接·(阻塞式)等待连接的到来
3.客户端套接字函数
s.connect() 主动初始化TCP服务器连接
s.connect_ex() connect()函数的扩展版本,出错时返回错误代码,而不是抛出异常
4.公共用途的套接字函数
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() 关闭套接字
5.面向锁的套接字方法
s.setblocking() 设置套接字阻塞与非阻塞模式
s.settimeout() 设置阻塞套接字操作的超时时间
s.gettimeout() 得到阻塞套接字的操作超时时间
6.面向文件的套接字函数
s.fileno() 套接字的文件描述符
s.makefile() 创建一个与该套接字相关的文件
三、基于TCP的套接字
(一)基本模板
1.tcp服务端及特性
ss = socket() #创建服务器套接字
ss.bind() #把地址绑定到套接字
ss.listen() #监听链接
inf_loop: #服务器无限循环
cs = ss.accept() #接受客户端链接
comm_loop: #通讯循环
cs.recv()/cs.send() #对话(接收与发送)
cs.close() #关闭客户端套接字
ss.close() #关闭服务器套接字(可选)
2.tcp客户端
cs = socket() # 创建客户套接字
cs.connect() # 尝试连接服务器
comm_loop: # 通讯循环
cs.send()/cs.recv() # 对话(发送/接收)
cs.close() # 关闭客户套接字
(二)一步步实现TCP套接字
1.基于tcp协议实现简单套接字通信
##——————————————————————————————————————server端基础版本
import socket
# 1.买手机
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # SOCK_STREAM =>TCP协议
# 2.插手机卡
phone.bind(('127.0.0.1', 8080)) # 本地回环,使用一个元组传参
# 3.开机
phone.listen(5)
print('starting %s:%s' % ('127.0.0.1', 8080))
# 4.等电话连接
conn, client_addr = phone.accept()
# 5.收/发消息
data = conn.recv(1024) # 最大接收的字节数
print('收到的客户端数据:', data.decode('utf-8'))
conn.send(data.upper())
# 6.关闭
conn.close()
phone.close()
##——————————————————————————————————————client端基础版本
import socket
# 1.买手机
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # SOCK_STREAM => TCP协议
# 2.拨电话
phone.connect(('127.0.0.1', 8080))
# 3.发/收消息
phone.send('hello'.encode('utf-8'))
data = phone.recv(1024)
print('服务的返回数据:', data.decode('utf-8'))
# 4.关闭
phone.close()
2.加上链接循环与通信循环
(1)为什么要加上加上链接循环与通信循环?
因为服务端应满足的特性:
① 一直对外提供服务;
② 并发地提供服务;
(2)加上链接循环与通信循环
##——————————————————————————————————————server端基础版本加上链接循环与通信循环
import socket
# 1.买手机
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # SOCK_STREAM =>TCP协议
# 2.插手机卡
phone.bind(('127.0.0.1', 8080)) # 本地回环,使用一个元组传参
# 3.开机
phone.listen(5) # 半连接池数量,队列
print('starting %s:%s' % ('127.0.0.1', 8080))
# 4.等电话连接===>>>链接循环(D)
# ##—————————————————此处的链接循环以后不可如此使用(需拆分开),这样只能一次处理一条服务,不能高并发
while True:
conn, client_addr = phone.accept()
print(client_addr)
# 5.收/发消息===>>>通信循环(A)
while True:
try:
data = conn.recv(1024) # 最大接收的字节数
# linux系统的一直收空信息的错误解决方法,判断是否为空,然后打破循环
if len(data) == 0:
break
# (B) conn是一个双向连接,若客户端非正常断开,则服务端此处会报错,因为通信无法完成,需加入异常管理
print('收到的客户端数据:', data.decode('utf-8'))
conn.send(data.upper())
except Exception: # windows系统的解决异常方法
break
# (C) 异常捕捉完成后,服务端不能结束,需要重新进行服务,则需要重新建立连接
# 6.关闭
conn.close() # 用于回收收无用地异常关闭地链接,然后进入连接循环,重新监听客户端地请求,暂无并发效果
phone.close()
# 用于软件正常关闭使用,暂时无用,正常写软件可使用关闭按钮实现
##——————————————————————————————————————client端基础版本加上链接循环与通信循环
import socket
# 1.买手机
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # SOCK_STREAM => TCP协议
# 2.拨电话
phone.connect(('127.0.0.1', 8080))
# 3.发/收消息===>>>通信循环
while True:
msg = input('>>>:').strip() # 用户输入消息进行通信
phone.send(msg.encode('utf-8'))
data = phone.recv(1024)
print('服务的返回数据:', data.decode('utf-8'))
# 4.关闭
phone.close()
(3)会遇到的问题以及解决方案
① 问题:报错地址仍在使用
原因:由于服务端仍然存在第四次挥手的time_wait状态在占用地址(相关知识:1.tcp三次握手,四次挥手;2.syn洪水攻击;3.服务器高并发情况下会有大量的time_wait状态的优化方法)
② 解决方案
(A)Windows下,加入一条socket配置,重用ip和端口
(此方法不推荐,会导致套接字不知道去哪,换端口号相对更好一些)
# 在服务端的绑定信息前面添加一条配置信息
# 2.插手机卡
phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 就是它,在bind前加
phone.bind(('127.0.0.1', 8080)) # 本地回环,使用一个元组传参
(B)Linux下,增加内核相关的配置,解决根本问题
发现系统存在大量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 时间
3.半连接池
未得到服务端连接的请求,都储存在半连接池中,当半连接池队列达到最大之后,后续的请求将无法得到响应,无法连接到服务器,除非半连接池队列数量减少,才能有新的请求进入半连接池。
并发量大的情况下,应该扩大半连接池,写入配置文件,使可调整,但是半连接池容量不能无限大,不可超过物理内存。
4.远程执行命令
(1)改写成可以远程执行命令的版本
使用subprocess模块,增加远程连接的功能
##——————————————————————————————————————server端基础版本加上链接循环与通信循环,再添加远程执行命令
import socket
import subprocess
# 1.买手机
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # SOCK_STREAM =>TCP协议
# 2.插手机卡
phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 就是它,在bind前加
phone.bind(('127.0.0.1', 8080)) # 本地回环,使用一个元组传参
# 3.开机
phone.listen(5) # 半连接池数量,队列
print('starting %s:%s' % ('127.0.0.1', 8080))
# 4.等电话连接===>>>链接循环
while True:
conn, client_addr = phone.accept()
print(client_addr)
# 5.收/发消息===>>>通信循环
while True:
try:
cmd = conn.recv(1024) # 最大接收的字节数
# linux系统的一直收空信息的错误解决方法,判断是否为空,然后打破循环
if len(cmd) == 0:
break
obj = subprocess.Popen(cmd.decode('utf-8'),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
res = obj.stdout.read()+obj.stderr.read() # 为何拼接?
print(res)
conn.send(res)
except Exception: # windows系统的解决异常方法
break
# (C) 异常捕捉完成后,服务端不能结束,需要重新进行服务,则需要重新建立连接
# 6.关闭
conn.close() # 用于回收收无用地异常关闭地链接,然后进入连接循环,重新监听客户端地请求,暂无并发效果
phone.close()
# 用于软件正常关闭使用,暂时无用,正常写软件可使用关闭按钮实现
##——————————————————————————————————————client端基础版本加上链接循环与通信循环,再添加远程执行命令
import socket
# 1.买手机
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # SOCK_STREAM => TCP协议
# 2.拨电话
phone.connect(('127.0.0.1', 8080))
# 3.发/收消息===>>>通信循环
while True:
cmd = input('[root@localhost]# ').strip() # 用户输入消息进行通信
phone.send(cmd.encode('utf-8'))
data = phone.recv(1024)
# windows系统下,应该使用gbk解码
print(data.decode('gbk'))
# 4.关闭
phone.close()