一:客户端/服务器 架构
1 1.硬件C/S架构:(例如,打印机) 2 2.软件C/S架构:互联网中处处是C/S架构 3 腾讯作为服务端为你提供视频,你得下个腾讯视频客户端才能看它的视频 4 5 C/S架构与socket的关系:socket就是为完成C/S架构的开发
二:什么是socket?
socket抽象层是位于应用层与运输层之间的,是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。
三:什么是套接字?
1 套接字起源于20世纪70年代加利福尼亚大学伯克利分校版本的 Unix,即人们所说的BSD Unix。 因此,有时人们也把套接字称为“伯克利套接字”或“BSD 套接字”。一开始,套接字被设计用在同一台主机上多个应用程序之间的通讯。这也被称进程间通讯,或 IPC。套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的。 2 3 基于文件类型的套接字家族: 4 套接字家族的名字:AF_UNIX 5 unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信。 6 7 基于网络类型的套接字家族: 8 套接字家族的名字:AF_INET 9 (还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET)。
tcp套接字工作流程:

先从服务器端说起。服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束

客户端: import socket phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.connect(('127.0.0.1',8000)) #拨通电话 ip地址+ 端口号 phone.send('hello'.encode('utf-8')) data=phone.recv(1024) print('收到的服务端发来的消息是:',data) 服务器端: import socket phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.bind(('127.0.0.1',8000)) phone.listen(5) print("----》") conn,addr=phone.accept() #等电话 msg=conn.recv(1024) #收消息 print('客户端发来的消息是:',msg) conn.send(msg.upper()) #发消息 conn.close() phone.close()
1 服务端套接字函数: 2 s.bind() 绑定(主机,端口号)到套接字 3 s.listen() 开始TCP监听 4 s.accept() 被动接受TCP客户的连接,(阻塞式)等待连接的到来 5 6 客户端套接字函数: 7 s.connect() 主动初始化TCP服务器连接 8 s.connect_ex() connect()函数的扩展版本,出错时返回出错码,而不是抛出异常 9 10 公共用途的套接字函数: 11 s.recv() 接收TCP数据 12 s.send() 发送TCP数据(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完) 13 s.sendall() 发送完整的TCP数据(本质就是循环调用send,sendall在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完) 14 s.recvfrom() 接收UDP数据 15 s.sendto() 发送UDP数据 16 s.getpeername() 连接到当前套接字的远端的地址 17 s.getsockname() 当前套接字的地址 18 s.getsockopt() 返回指定套接字的参数 19 s.setsockopt() 设置指定套接字的参数 20 s.close() 关闭套接字 21 22 面向锁的套接字方法: 23 s.setblocking() 设置套接字的阻塞与非阻塞模式 24 s.settimeout() 设置阻塞套接字操作的超时时间 25 s.gettimeout() 得到阻塞套接字操作的超时时间 26 27 面向文件的套接字的函数: 28 s.fileno() 套接字的文件描述符 29 s.makefile() 创建一个与该套接字相关的文件
(tcp是基于链接的,必须先启动服务端,然后再启动客户端去链接服务端)
三次握手,四次挥手:
客户端,服务端循环收发消息:
1 服务端: 2 from socket import * 3 ip_port=('10.253.69.142',55102) 4 back_log=5 5 buffer_size=1024 6 7 tcp_server=socket(AF_INET,SOCK_STREAM) 8 tcp_server.bind(ip_port) 9 tcp_server.listen(back_log) 10 11 conn,addr=tcp_server.accept() 12 print('双向链接是',conn) 13 print('客户端地址是',addr) 14 15 while True: 16 data=conn.recv(buffer_size) 17 print('客户端发来的消息是',data.decode('utf-8')) 18 conn.send(data.upper()) 19 20 conn.close() 21 tcp_server.close() 22 23 客户端: 24 from socket import * 25 ip_port=('10.253.69.142',55102) 26 back_log=5 27 buffer_size=1024 28 29 tcp_client=socket(AF_INET,SOCK_STREAM) 30 tcp_client.bind(ip_port) 31 32 while True: 33 msg=input('>>:').strip() 34 tcp_client.send(msg.encode('utf-8')) 35 print('客户端已发送消息') 36 data=tcp_client.recv(buffer_size) 37 print('收到服务端发来的消息') 38 39 tcp_client.close()
四:基于UDP的套接字 (udp是无链接的,不管先启动哪一端都可以)
1 客户端: 2 from socket import * 3 ip_port=('172.29.89.106',8080) 4 buffer_size=1024 5 6 udp_client=socket(AF_INET,SOCK_DGRAM) #数据报 7 8 while True: 9 msg=input('>>:').strip() 10 udp_client.sendto(msg.encode('utf-8'),ip_port) #sendto括号后面必须加ip_port 11 12 data,addr=udp_client.recvfrom(buffer_size) 13 print(data.decode('utf-8')) 14 15 服务端: 16 from socket import * 17 ip_port=('172.29.89.106',8080) 18 buffer_size=1024 19 20 udp_server=socket(AF_INET,SOCK_DGRAM) #数据报 21 udp_server.bind(ip_port) 22 23 while True: 24 data,addr=udp_server.recvfrom(buffer_size) 25 print(data) 26 27 udp_server.sendto(data.upper(),addr)
时间服务器:
1 客户端: 2 from socket import * 3 ip_port=('172.29.89.106',8080) 4 buffer_size=1024 5 6 tcp_client=socket(AF_INET,SOCK_DGRAM) 7 8 while True: 9 msg=input('请输入时间格式(%Y %m %d)>>: ').strip() 10 tcp_client.sendto(msg.encode('utf-8'),ip_port) 11 12 data=tcp_client.recv(buffer_size) 13 14 print(data.decode('utf-8')) 15 16 tcp_client.close() 17 18 服务端: 19 from socket import * 20 from time import strftime 21 22 ip_port = ('172.29.89.106', 8080) 23 buffer_size = 1024 24 25 tcp_server = socket(AF_INET, SOCK_DGRAM) 26 tcp_server.bind(ip_port) 27 28 while True: 29 msg, addr = tcp_server.recvfrom(buffer_size) 30 print('===>', msg) 31 32 if not msg: 33 time_fmt = '%Y-%m-%d %X' 34 else: 35 time_fmt = msg.decode('utf-8') 36 back_msg = strftime(time_fmt) 37 38 tcp_server.sendto(back_msg.encode('utf-8'), addr) 39 40 tcp_server.close()
五:粘包
只有tcp会出现粘包的问题,udp不会
所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的
- TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法,将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
- UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
- tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头
- udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一 一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠
- tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。
以下两种情况下会发生粘包现象:
1 发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)
1 服务端: 2 from socket import * 3 ip_port=('127.0.0.1',8080) 4 5 tcp_socket_server=socket(AF_INET,SOCK_STREAM) 6 tcp_socket_server.bind(ip_port) 7 tcp_socket_server.listen(5) 8 9 10 conn,addr=tcp_socket_server.accept() 11 12 data1=conn.recv(10) 13 data2=conn.recv(10) 14 15 print('----->',data1.decode('utf-8')) 16 print('----->',data2.decode('utf-8')) 17 18 conn.close() 19 20 客户端: 21 import socket 22 BUFSIZE=1024 23 ip_port=('127.0.0.1',8080) 24 25 s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) 26 res=s.connect_ex(ip_port) 27 28 s.send('hello'.encode('utf-8')) 29 s.send('feng'.encode('utf-8'))
1 接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包
1 服务端: 2 from socket import * 3 ip_port=('127.0.0.1',8080) 4 5 tcp_socket_server=socket(AF_INET,SOCK_STREAM) 6 tcp_socket_server.bind(ip_port) 7 tcp_socket_server.listen(5) 8 9 conn,addr=tcp_socket_server.accept() 10 11 data1=conn.recv(2) #一次没有收完整 12 data2=conn.recv(10)#下次收的时候,会先取旧的数据,然后取新的 13 14 print('----->',data1.decode('utf-8')) 15 print('----->',data2.decode('utf-8')) 16 17 conn.close() 18 19 客户端: 20 import socket 21 BUFSIZE=1024 22 ip_port=('127.0.0.1',8080) 23 24 s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) 25 res=s.connect_ex(ip_port) 26 27 s.send('hello feng'.encode('utf-8'))