socket编程基本流程及TCP套接字
一 socket编程基本流程
1 套接字的概念及分类
1.1 套接字是什么
套接字是一种【通信机制】,凭借这种机制,客户/服务器系统的开发工作既可以在本地单机上进行,也可以跨网络进行,Linux所提供的功能(如打印服务,ftp等) 通常都是通过套接字来进行通信的,套接字的创建和使用与管道是有区别的,因为套接字明确地将客户和服务器区分出来,套接字可以实现将多个客户连接到一个服务器。
套接字,也称为BSD套接字,是支持TCP/IP的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。应用层通过传输层进行数据通信时,TCP和UDP会遇到同时为多个应用程序进程提供并发服务的问题。
简单的举例说明:Socket=Ip address+ TCP/UDP + port。
1.2 套接字的分类
# (1) 基于文件类型
套接字家族的名字:AF_UNIX。
# (2) 基于网络类型
套接字家族的名字:AF_INET。
2 套接字工作流程
2.1 面向连接的套接字Socket通信工作流程
1).服务器先用socket函数来建立一个套接字,用这个套接字完成通信的监听;
2).用bind函数来绑定一个端口号和一个IP地址。因为本地计算机可能有多个网址和IP,每一个IP有多个端口,需要指定一个IP和端口进行监听。
3).服务器调用listen函数,使服务器的这个端口和IP处于监听状态,等待客户机的连接。
4).客户机用socket函数建立一个套接字,设定远程IP和端口。
5).客户机调用connect函数连接远程计算机指定的端口。
6).服务器用accept函数来接受远程计算机的连接,建立起与客户机之间的通信。
7).建立连接以后,客户机用write函数想socket中写入数据。也可以用read函数读取服务器发送来的数据。
8).服务器用read函数读取客户机发送来的数据,也可以用write函数来发送数据。
9).完成通信以后,用close函数关闭socket连接。

2.2 面向无连接的套接字Socket通信工作流程
无连接的通信不需要建立起客户机与服务器之间的连接,因此在程序中没有简历连接的过程。进行通信之前,需要简历网络套接字。服务器需要绑定一个端口,在这个端口上监听收到的信息。客户机需要设置远程IP和端口,需要传递的信息需要发送到这个IP和端口上。

2.3 socket() 模块函数用法
2.3.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.3.2 服务端套接字函数
s.bind() # 绑定(主机、端口号) 到套接字
s.listen() # 开始TCP监听
s.accept() # 被动接受TCP客户的连接·(阻塞式) 等待连接的到来
2.3.3 客户端套接字函数
s.connect() # 主动初始化TCP服务器连接
s.connect_ex() # connect()函数的扩展版本,出错时返回错误代码,而不是抛出异常
2.3.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() # 关闭套接字
2.3.5 面向锁的套接字方法
s.setblocking() # 设置套接字阻塞与非阻塞模式s.settimeout() # 设置阻塞套接字操作的超时时间s.gettimeout() # 得到阻塞套接字的操作超时时间
2.3.6 面向文件的套接字函数
s.fileno() # 套接字的文件描述符s.makefile() # 创建一个与该套接字相关的文件
二 基于TCP的套接字
1 基本模板
1.1 tcp服务端及特性
ss = socket() # 创建服务器套接字ss.bind() # 把地址绑定到套接字ss.listen() # 监听链接inf_loop: # 服务器无限循环 cs = ss.accept() # 接受客户端链接 comm_loop: # 通讯循环 cs.recv()/cs.send() # 对话(接收与发送) cs.close() # 关闭客户端套接字ss.close() # 关闭服务器套接字(可选)
1.2 tcp客户端
cs = socket() # 创建客户套接字cs.connect() # 尝试连接服务器comm_loop: # 通讯循环 cs.send()/cs.recv() # 对话(发送/接收)cs.close() # 关闭客户套接字
2 一步步实现TCP套接字
2.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.2 加上链接循环与通信循环
2.2.1 为什么要加上加上链接循环与通信循环?
# 因为服务端应满足的特性: ① 一直对外提供服务; ② 并发地提供服务;
2.2.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()
2.2.3 会遇到的问题以及解决方案
2.2.3.1 问题:报错地址仍在使用

原因:由于服务端仍然存在第四次挥手的**time_wait**状态在占用地址(相关知识:1.tcp三次握手,四次挥手;2.syn洪水攻击;3.服务器高并发情况下会有大量的time_wait状态的优化方法)
2.2.3.2 解决方案
###### (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 时间
2.3 半连接池
未得到服务端连接的请求,都储存在半连接池中,当半连接池队列达到最大之后,后续的请求将无法得到响应,无法连接到服务器,除非半连接池队列数量减少,才能有新的请求进入半连接池。
并发量大的情况下,应该扩大半连接池,写入配置文件,使可调整,但是半连接池容量不能无限大,不可超过物理内存。
2.4 远程执行命令
# 改写成可以远程执行命令的版本
# 使用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()