一花一世界,一叶一菩提。
套接字(socket)编程历史
上图就是我们所抽象出来的套接字层:
也就是我们所学习的那些计算机语言给我们已经封装或者打包好的一些我们可以直接使用的套接字功能。
什么是套接字(socket)
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。
套接字的发展史以及分类:
套接字起源于 20 世纪 70 年代加利福尼亚大学伯克利分校版本的 Unix,即人们所说的 BSD Unix。 因此,有时人们也把套接字称为“伯克利套接字”或“BSD 套接字”。一开始,套接字被设计用在同 一台主机上多个应用程序之间的通讯。这也被称进程间通讯,或 IPC。套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的。
基于文件类型的套接字家族
套接字家族的名字:AF_UNIX
unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信
基于网络类型的套接字家族
套接字家族的名字:AF_INET
(还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET)
套接字的工作流程
一个生活中的场景。你要打电话给一个朋友,先拨号,朋友听到电话铃声后提起电话,这时你和你的朋友就建立起了连接,就可以讲话了。等交流结束,挂断电话结束此次交谈。 生活中的场景就解释了这工作原理。
先从服务器端说起。服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束
套接字(socket)模块函数的用法
import socket
socket.socket(socket_family,socket_type,protocal=0)
socket_family
#可以是 AF_UNIX 或 AF_INET。socket_type 可以是 SOCK_STREAM 或 SOCK_DGRAM。protocol 一般不填,默认值为 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)
服务端套接字函数:
s.bind() #绑定(主机,端口号)到套接字
s.listen() #开始TCP监听
s.accept() #被动接受TCP客户的连接,(阻塞式)等待连接的到来
客户端套接字函数:
s.connect() #主动初始化TCP服务器连接
s.connect_ex() #connect()函数的扩展版本,出错时返回出错码,而不是抛出异常
公共用途的套接字函数:
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() #关闭套接字
面向锁的套接字方法:
s.setblocking() #设置套接字的阻塞与非阻塞模式
s.settimeout() #设置阻塞套接字操作的超时时间
s.gettimeout() #得到阻塞套接字操作的超时时间
面向文件的套接字函数:
s.fileno() #套接字的文件描述符
s.makefile() #创建一个与该套接字相关的文件
TCP套接字编程
TCP的服务端套接字编程:
ss = socket() #创建服务器套接字
ss.bind() #把地址绑定到套接字
ss.listen() #监听链接
inf_loop: #服务器无限循环
cs = ss.accept() #接受客户端链接
comm_loop: #通讯循环
cs.recv()/cs.send() #对话(接收与发送)
cs.close() #关闭客户端套接字
ss.close() #关闭服务器套接字(可选)
TCP的客户端套接字编程:
1 cs = socket() # 创建客户套接字
2 cs.connect() # 尝试连接服务器
3 comm_loop: # 通讯循环
4 cs.send()/cs.recv() # 对话(发送/接收)
5 cs.close() # 关闭客户套接字
完整版的来一波:
TCP服务端和客户端:
服务端:
#!/usr/bin/env python
# -*- coding:utf-8 -*-
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() # 关机
客户端:
#!/usr/bin/env python
# -*- coding:utf-8 -*-
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()
运行结果:客户端
运行结果:服务端
我们通过发现可以看出,这样的服务端和客户端不是我们所希望的那种:
服务端应该可以不间断运行的。所以我们进行循环优化(请看下述代码):
加循环TCP下的套接字编程
服务端:
#!/usr/bin/env python
# -*- coding:utf-8 -*-
"""
服务端应该满足的特性:
1、一直对外提供服务
2、并发地提供服务
"""
import socket
# 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:
data = conn.recv(1024) # 最大接收的字节个数
if len(data) == 0: # 针对linux系统
break
print("收到的客户端数据:", data.decode('utf-8'))
conn.send(data.upper())
except Exception: # 针对windows系统
break
# 6、关闭
conn.close() # 挂电话
phone.close() # 关机
客户端:
#!/usr/bin/env python
# -*- coding:utf-8 -*-
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()
运行结果:客户端
运行结果:服务端
到现在这一步我们可以看出一些功能我们已经可以实现了。
面向外网
既然是面向外网,那么肯定是可以让其他网络上的客户端来访问我们。
来这里我们想一想,前面我么您所输入的ip地址是什么
ip地址 127.0.0.1 --------> 内部计算机的 回送地址
那么我们现在需要改成什么呢?
那就是我们现在的IP地址了:
改完以后我们进行测试:
TCP协议下的外网测试:
#!/usr/bin/env python
# -*- coding:utf-8 -*-
"""
服务端应该满足的特性:
1、一直对外提供服务
"""
import socket
# 1、买手机
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# SOCK_STREAM=》TCP协议
# 2、插手机卡
phone.bind(("192.168.11.183", 8888)) # 本地回环
# 3、开机
phone.listen(5)
print('starting 服务器的IP地址:%s 端口号:%s' %("192.168.11.183", 8888))
# 4、等电话链接=>链接循环
while True:
conn, client_addr = phone.accept()
print(client_addr) # 对方的地址信息我们打印一下。
# 5、收/发消息=>通信循环
while True:
try:
data = conn.recv(1024) # 最大接收的字节个数
print("收到的客户端数据:", data.decode('utf-8'))
conn.send(data.upper())
except Exception:
break
# 6、关闭
conn.close() # 挂电话
phone.close() # 关机
测试结果:
代码中会遇到的问题:
报错:如果客户端突然关闭了
报错内容:
Traceback (most recent call last):
File "E:/code/s15-day31/02 加上循环/02 服务端.py", line 26, in <module>
data = conn.recv(1024) # 最大接收的字节个数
ConnectionResetError: [WinError 10054] 远程主机强迫关闭了一个现有的连接。
错误原因:
mport socket
# 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:
data = conn.recv(1024) # 最大接收的字节个数
if len(data) == 0: # 针对linux系统
break
print("收到的客户端数据:", data.decode('utf-8'))
conn.send(data.upper())
# 6、关闭
conn.close() # 挂电话
phone.close() # 关机
错误原因很简答,就是报错为捕捉:
==需要加上try:==具体代码就是前面的。
报错说端口问题:
有的同学在重启服务端时可能会遇到
这个是由于你的服务端仍然存在四次挥手的time_wait状态在占用地址
(如果不懂,请深入研究1.tcp三次握手,四次挥手 2.syn洪水攻击 3.服务器高并发情况下会有大量的time_wait状态的优化方法)
**解决方法:一 **
#加入一条socket配置,重用ip和端口
phone=socket(AF_INET,SOCK_STREAM)
phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加
phone.bind(('127.0.0.1',8080))
**解决方法:二 **
发现系统存在大量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 时间
方法二
远程关闭计算机:
我们也可以做远程关闭计算机的操作,就是需要将服务端以管理员权限运行起来。
cmd中代码:
- 关机 shutdown -s -t 0
- 重启 shutdown -r -t 0