Socket学习笔记(一)
1.socket介绍
我们知道两个进程如果需要进行通讯最基本的一个前提能能够唯一的标示一个进程,在本地进程通讯中我们可以使用PID来唯一标示一个进程,但PID只在本地唯一,网络中的两个进程PID冲突几率很大,这时候我们需要另辟它径了,我们知道IP层的ip地址可以唯一标示主机,而TCP层协议和端口号可以唯一标示主机的一个进程,这样我们可以利用ip地址+协议+端口号唯一标示网络中的一个进程。
能够唯一标示网络中的进程后,它们就可以利用socket进行通信了。
什么是socket呢?我们经常把socket翻译为套接字,socket是在应用层和传输层之间的一个抽象层,它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用已实现进程在网络中通信。
socket起源于UNIX,在Unix一切皆文件哲学的思想下,socket是一种"打开—读/写—关闭"模式的实现,服务器和客户端各自维护一个"文件",在建立连接打开后,可以向自己文件写入内容供对方读取或者读取对方内容,通讯结束时关闭文件。
2.套接字发展史
套接字起源于 20 世纪 70 年代加利福尼亚大学伯克利分校版本的 Unix,即人们所说的 BSD Unix。 因此,有时人们也把套接字称为“伯克利套接字”或“BSD 套接字”。一开始,套接字被设计用在同 一台主机上多个应用程序之间的通讯。这也被称进程间通讯,或 IPC。套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的。
基于文件类型的套接字家族
套接字家族的名字:AF_UNIX
unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信(IPC)。
基于网络类型的套接字家族套接字家族的名字:AF_INET
AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET。
3.套接字工作流程
4.socket模块函数
1.socket(socket_family, socket_type, protocol=0)
socket_family: AF_UNIX 或 AF_INET。
socket_type:SOCK_STREAM(TCP) 或 SOCK_DGRAM(UDP)
protocol 一般不填,默认值为 0
- 创建一个 TCP/IP 的套接字:
TcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- 创建一个 UDP/IP 的套接字:
UdpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
使用技巧:使用'from socket import *',我们就把 socket 模块里的所有属性都带到我们的命名空间里了,这样能大幅减短我们的代码。
2.套接字对象(内建)方法
- 服务器端套接字函数
s.bind() 绑定地址(主机,端口号对)到套接字
s.listen() 开始 TCP 监听
s.accept() 被动接受 TCP 客户的连接, (阻塞式)等待连接的到来
- 客户端套接字函数
s.connect() 主动初始化TCP服务器连接
s.connect_ex() connect()函数的扩展版本,出错时返回出错码,而不是抛出异常
- 公共用途的套接字函数
s.getpeername() 连接到当前套接字的远端的地址
s.getsockname() 当前套接字的地址
s.getsockopt() 返回指定套接字的参数
s.setsockopt() 设置指定套接字的参数
s.close() 关闭套接字
s.send() 发送 TCP 数据 Return the number of bytes sent; this may be less than len(data) if the network is busy.
s.sendall() 完整发送 TCP 数据 This calls send() repeatedly until all data is sent. If an error occurs, it's impossible to tell how much data has been sent.
s.recv() 接收 TCP 数据
s.recvfrom() 接收 UDP 数据
s.sendto() 发送 UDP 数据
面向锁的套接字方法
s.setblocking() 设置套接字的阻塞与非阻塞模式
s.settimeout() 设置阻塞套接字操作的超时时间
s.gettimeout() 得到阻塞套接字操作的超时时间
面向文件的套接字的函数
s.fileno() 套接字的文件描述符
s.makefile() 创建一个与该套接字相关的文件
5.基于TCP的套接字编程
TCP服务端
ss = socket() #创建服务器套接字 ss.bind() #把地址绑定到套接字 ss.listen() #监听链接 inf_loop: #服务器无限循环 cs = ss.accept() #接受客户端链接 comm_loop: #通讯循环 cs.recv()/cs.send() #对话(接收与发送) cs.close() #关闭客户端套接字 ss.close() #关闭服务器套接字(可选)
TCP客户端
cs = socket() # 创建客户套接字 cs.connect() # 尝试连接服务器 comm_loop: # 通讯循环 cs.send()/cs.recv() # 对话(发送/接收) cs.close() # 关闭客户套接字
代码示例:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
# 服务端 from socket import * ip_port = ('localhost', 8081) BUFSIZE = 1024 s=socket(AF_INET,SOCK_STREAM) s.bind(ip_port) s.listen(5) while True: conn, addr = s.accept() while True: msg = conn.recv(BUFSIZE) if len(msg) == 0: break #如果不加,那么正在链接的客户端突然断开,recv便不再阻塞,死循环发生 print(msg) conn.send(msg.upper()) conn.close() s.close() # 客户端 from socket import * ip_port = ('127.0.0.1', 8081) BUFSIZE = 1024 s = socket(AF_INET, SOCK_STREAM) s.connect_ex(ip_port) while True: msg = input('>>: ').strip() if len(msg) == 0:continue s.send(msg.encode('utf-8')) feedback = s.recv(BUFSIZE) print(feedback.decode('utf-8')) s.close()
如果在连接过程中遇到端口已被占用,解决办法:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#加入一条socket配置,重用ip和端口 phone=socket(AF_INET,SOCK_STREAM) phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加 phone.bind(('127.0.0.1',8080))
6.基于UDP的套接字编程
UDP服务端
ss = socket() #创建一个服务器的套接字 ss.bind() #绑定服务器套接字 inf_loop: #服务器无限循环 cs = ss.recvfrom()/ss.sendto() ss.close()
UDP客户端
cs = socket() comm_loop: # 通讯循环 cs.sendto()/cs.recvfrom() cs.close()
代码示例:(UDP不用建立链接)
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
# 服务端 from socket import * ip_port = ('localhost',9000) BUF = 1024 udp_server = socket(AF_INET,SOCK_DGRAM) udp_server.bind(ip_port) while True: msg,addr = udp_server.recvfrom(BUF) print(msg,addr) udp_server.sendto(msg.upper(),addr) # 客户端 from socket import * ip_port = ('localhost',9000) BUF = 1024 udp_client = socket(AF_INET,SOCK_DGRAM) while True: msg = input(">>>>:").strip() if not msg:continue udp_client.sendto(msg.encode('utf-8'), ip_port) back_msg,addr = udp_client.recvfrom(BUF) print(back_msg.decode('utf-8'),addr)
模拟QQ聊天
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
# 服务端 import socket ip_port=('127.0.0.1',8081) udp_server_sock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) udp_server_sock.bind(ip_port) while True: qq_msg,addr=udp_server_sock.recvfrom(1024) print('来自[%s:%s]的一条消息:\033[1;44m%s\033[0m' %(addr[0],addr[1],qq_msg.decode('utf-8'))) back_msg=input('回复消息: ').strip() udp_server_sock.sendto(back_msg.encode('utf-8'),addr) # 客户端 import socket BUFSIZE=1024 udp_client_socket=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) qq_name_dic={ '悟空':('127.0.0.1',8081), '八戒':('127.0.0.1',8081), '沙和尚':('127.0.0.1',8081), '小白龙':('127.0.0.1',8081), } while True: qq_name=input('请选择聊天对象: ').strip() while True: msg=input('请输入消息,回车发送: ').strip() if msg == 'quit':break if not msg or not qq_name or qq_name not in qq_name_dic:continue udp_client_socket.sendto(msg.encode('utf-8'),qq_name_dic[qq_name]) back_msg,addr=udp_client_socket.recvfrom(BUFSIZE) print('来自[%s:%s]的一条消息:\033[1;44m%s\033[0m' %(addr[0],addr[1],back_msg.decode('utf-8'))) udp_client_socket.close()
基于UDP协议发送数据的方式,不用建立双向链接。所以,可以同时链接多个客户端进行通信。
7.Tcp与Udp几个函数之间的区别
发消息,都是将数据发送到自己端操作系统的缓冲区中,收消息都是从自己端操作系统的缓冲区中取出数据。
tcp:send发消息,recv收消息
udp:sendto发消息,recvfrom收消息
- send与sendto
tcp是基于数据流的,而udp是基于数据报的:
send(bytes_data):发送数据流,数据流bytes_data若为空,自己这段的缓冲区也为空,操作系统不会控制tcp协议发空包
sendinto(bytes_data,ip_port):发送数据报,bytes_data为空,还有ip_port,所有即便是发送空的bytes_data,数据报其实也不是空的,自己这端的缓冲区收到内容,操作系统就会控制udp协议发包。
- recv与recvfrom
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
tcp协议: (1)如果收消息缓冲区里的数据为空,那么recv就会阻塞(阻塞很简单,就是一直在等着收) (2)只不过tcp协议的客户端send一个空数据就是真的空数据,客户端即使有无穷个send空,也跟没有一个样。 (3)tcp基于链接通信 基于链接,则需要listen(backlog),指定半连接池的大小 基于链接,必须先运行的服务端,然后客户端发起链接请求 对于mac系统:如果一端断开了链接,那另外一端的链接也跟着完蛋recv将不会阻塞,收到的是空(解决方法是:服务端在收消息后加上if判断,空消息就break掉通信循环) 对于windows/linux系统:如果一端断开了链接,那另外一端的链接也跟着完蛋recv将不会阻塞,收到的是空(解决方法是:服务端通信循环内加异常处理,捕捉到异常后就break掉通讯循环)
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
udp协议 (1)如果如果收消息缓冲区里的数据为“空”,recvfrom也会阻塞 (2)只不过udp协议的客户端sendinto一个空数据并不是真的空数据(包含:空数据+地址信息,得到的报仍然不会为空),所以客户端只要有一个sendinto(不管是否发送空数据,都不是真的空数据),服务端就可以recvfrom到数据。 (3)udp无链接 无链接,因而无需listen(backlog),更加没有什么连接池之说了 无链接,udp的sendinto不用管是否有一个正在运行的服务端,可以己端一个劲的发消息,只不过数据丢失 recvfrom收的数据小于sendinto发送的数据时,在mac和linux系统上数据直接丢失,在windows系统上发送的比接收的大直接报错 只有sendinto发送数据没有recvfrom收数据,数据丢失
基于TCP通信,如果客户端发送消息为空,那么服务端和客户端都会卡在recv状态,等待接受消息。如果客户端直接终止,那么就会报错。
而如果基于UDP通信,客户端发送消息为空,服务端会接受到一个空串。单独运行上面的udp的客户端,你发现并不会报错,相反tcp却会报错,因为udp协议只负责把包发出去,对方收不收,我根本不管,而tcp是基于链接的,必须有一个服务端先运行着,客户端去跟服务端建立链接然后依托于链接才能传递消息,任何一方试图把链接摧毁都会导致对方程序的崩溃。而且,UDP通信中,服务端有几个recvfrom就要对应几个sendinto,如果没有相应数量的与之对应,就会处于等待状态。