1、客户端(client)服务端(sever)架构python
在计算机中有不少常见的C/S架构,例如咱们的浏览器是客户端、而百度网站和其余的网站就是服务端;视频软件是客户端,提供视频的腾讯、优酷、爱奇艺就是服务端。程序员
C/S与socket的关系:shell
学习socket就是为了开发C/S架构。编程
2、OSI七层设计模式
C/S架构的软件(软件属于应用层)是基于网络进行通讯的,网络的核心即一堆协议,协议即标准,你想开发一款基于网络通讯的软件,就必须遵循这些标准。因此在学习socket以前,先了解一下OSI七层了解基本的网络协议,方便学习socket。浏览器
在上面的OSI中好像并无与socket有关的信息,那么请看下面这图:缓存
3、socket是什么服务器
从上面这个图中能够看出,socket就是网络层和运输层的抽象结合。Socket是应用层与TCP/IP协议族通讯的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来讲,一组简单的接口就是所有,让Socket去组织数据,以符合指定的协议。因此,咱们无需深刻理解tcp/udp协议,socket已经为咱们封装好了,咱们只须要遵循socket的规定去编程,写出的程序天然就是遵循tcp/udp标准的。网络
固然还有另外一种解释:网络上的两个程序经过一个双向的通讯链接实现数据的交换,这个链接的一端称为一个socket。创建网络通讯链接至少要一对端口号(socket)。socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员作网络开发所用的接口,这就是Socket编程接口;HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通讯的能力。Socket的英文原义是"孔"或"插座"。架构
做为BSD UNIX的进场通讯机制,取后一种意思。一般也称做"套接字",用于描述IP地址和端口,是一个通讯链的句柄,能够用来实现不一样虚拟机或不一样计算机之间的通讯。在Internet上的主机通常运行了多个服务软件,同时提供几种服务。每种服务都打开一个Socket,并绑定到一个端口上,不一样的端口对应于不一样的服务。Socket正如其英文原意那样,像一个多孔插座。一台主机犹如布满各类插座的房间,每一个插座有一个编号,有的插座提供220伏交流电, 有的提供110伏交流电,有的则提供有线电视节目。 客户软件将插头插到不一样编号的插座,就能够获得不一样的服务。
套接字起源于 20 世纪 70 年代加利福尼亚大学伯克利分校版本的 Unix,即人们所说的 BSD Unix。 所以,有时人们也把套接字称为“伯克利套接字”或“BSD 套接字”。一开始,套接字被设计用在同 一台主机上多个应用程序之间的通信。这也被称进程间通信,或 IPC。套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的。
基于文件类型的套接字家族
套接字家族的名字:AF_UNIX
unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,能够经过访问同一个文件系统间接完成通讯
基于网络类型的套接字家族套接字家族的名字:AF_INET
(还有AF_INET6被用于ipv6,还有一些其余的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是不多被使用,或者是根本没有实现,全部地址家族中,AF_INET是使用最普遍的一个,python支持不少种地址家族,可是因为咱们只关心网络编程,因此大部分时候我么只使用AF_INET)
5、套接字工做流程
根据链接启动的方式以及本地套接字要链接的目标,套接字之间的链接过程能够分为三个步骤:服务器监听,客户端请求,链接确认。
(1)服务器监听:是服务器端套接字并不定位具体的客户端套接字,而是处于等待链接的状态,实时监控网络状态。
(2)客户端请求:是指由客户端的套接字提出链接请求,要链接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要链接的服务器的套接字,指出服务器端套接字的地址和端口号,而后就向服务器端套接字提出链接请求。
(3)链接确认:是指当服务器端套接字监听到或者说接收到客户端套接字的链接请求,它就响应客户端套接字的请求,创建一个新的线程,把服务端套接字的描述发给客户端,一旦客户端确认了此描述,链接就创建好了。而服务器端套接字继续处于监听状态,继续接收其余客户端套接字的链接请求。
生活中的打电话就是一个简单的套接字工做流程:
5、常见的套接字函数:
服务端套接字函数
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() 建立一个与该套接字相关的文件
6、基于TCP的套接字
在实现TCP的套接字以前,小编带你们了解一下基于TCP的三次握手,四次挥手。
TCP是面向链接的,不管哪一方向另外一方发送数据以前,都必须先在双方之间创建一条链接。在TCP/IP协议中,TCP 协议提供可靠的链接服务,链接是经过三次握手进行初始化的。三次握手的目的是同步链接双方的序列号和确认号 并交换 TCP窗口大小信息。
1.第一次握手:创建链接。
客户端发送链接请求报文段,将SYN位置为1,Sequence Number为x;而后,客户端进入SYN_SEND状态,等待服务器的确认;
2.第二次握手:服务器收到SYN报文段。
服务器收到客户端的SYN报文段,须要对这个SYN报文段进行确认,设置Acknowledgment Number为x+1(Sequence Number+1);同时,本身本身还要发送SYN请求信息,将SYN位置为1,Sequence Number为y;服务器端将上述全部信息放到一个报文段(即SYN+ACK报文段)中,一并发送给客户端,此时服务器进入SYN_RECV状态;
3.第三次握手:客户端收到服务器的SYN+ACK报文段。
而后将Acknowledgment Number设置为y+1,向服务器发送ACK报文段,这个报文段发送完毕之后,客户端和服务器端都进入ESTABLISHED状态,完成TCP三次握手。
完成了三次握手,客户端和服务器端就能够开始传送数据。以上就是TCP三次握手的整体介绍。
那四次挥手呢?
当客户端和服务器经过三次握手创建了TCP链接之后,当数据传送完毕,确定是要断开TCP链接的啊。那对于TCP的断开链接,这里就有了神秘的“四次挥手”。
1.第一次挥手:主机1(可使客户端,也能够是服务器端),设置Sequence Number和Acknowledgment Number,向主机2发送一个FIN报文段;此时,主机1进入FIN_WAIT_1状态;这表示主机1没有数据要发送给主机2了;
2.第二次挥手:主机2收到了主机1发送的FIN报文段,向主机1回一个ACK报文段,Acknowledgment Number为Sequence Number加1;主机1进入FIN_WAIT_2状态;主机2告诉主机1,我也没有数据要发送了,能够进行关闭链接了;
3.第三次挥手:主机2向主机1发送FIN报文段,请求关闭链接,同时主机2进入CLOSE_WAIT状态;
4.第四次挥手:主机1收到主机2发送的FIN报文段,向主机2发送ACK报文段,而后主机1进入TIME_WAIT状态;主机2收到主机1的ACK报文段之后,就关闭链接;此时,主机1等待2MSL后依然没有收到回复,则证实Server端已正常关闭,那好,主机1也能够关闭链接了。
至此,TCP的四次挥手就这么愉快的完成了。当你看到这里,你的脑子里会有不少的疑问,不少的不懂,感受很凌乱;没事,咱们继续总结。
为何要三次握手?
既然总结了TCP的三次握手,那为何非要三次呢?怎么以为两次就能够完成了。那TCP为何非要进行三次链接呢?在谢希仁的《计算机网络》中是这样说的:
为了防止已失效的链接请求报文段忽然又传送到了服务端,于是产生错误。
在书中同时举了一个例子,以下:
"已失效的链接请求报文段”的产生在这样一种状况下:client发出的第一个链接请求报文段并无丢失,
而是在某个网络结点长时间的滞留了,以至延误到链接释放之后的某个时间才到达server。原本这是一
个早已失效的报文段。但server收到此失效的链接请求报文段后,就误认为是client再次发出的一个新
的链接请求。因而就向client发出确认报文段,赞成创建链接。假设不采用“三次握手”,那么只要server
发出确认,新的链接就创建了。因为如今client并无发出创建链接的请求,所以不会理睬server的确认,
也不会向server发送数据。但server却觉得新的运输链接已经创建,并一直等待client发来数据。这样,
server的不少资源就白白浪费掉了。采用“三次握手”的办法能够防止上述现象发生。例如刚才那种状况,
client不会向server的确认发出确认。server因为收不到确认,就知道client并无要求创建链接。"
这就很明白了,防止了服务器端的一直等待而浪费资源。
为何要四次挥手?
那四次挥手又是为什么呢?TCP协议是一种面向链接的、可靠的、基于字节流的运输层通讯协议。TCP是全双工 模式,这就意味着,当主机1发出FIN报文段时,只是表示主机1已经没有数据要发送了,主机1告诉主机2, 它的数据已经所有发送完毕了;可是,这个时候主机1仍是能够接受来自主机2的数据;当主机2返回ACK报文 段时,表示它已经知道主机1没有数据发送了,可是主机2仍是能够发送数据到主机1的;当主机2也发送了FIN 报文段时,这个时候就表示主机2也没有数据要发送了,就会告诉主机1,我也没有数据要发送了,以后彼此 就会愉快的中断此次TCP链接。若是要正确的理解四次挥手的原理,就须要了解四次挥手过程当中的状态变化。
FIN_WAIT_1: 这个状态要好好解释一下,其实FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等 待对方的FIN报文。而这两种状态的区别是:FIN_WAIT_1状态其实是当SOCKET在ESTABLISHED状态时, 它想主动关闭链接,向对方发送了FIN报文,此时该SOCKET即进入到FIN_WAIT_1状态。而当对方回应ACK报 文后,则进入到FIN_WAIT_2状态,固然在实际的正常状况下,不管对方何种状况下,都应该立刻回应ACK 报文,因此FIN_WAIT_1状态通常是比较难见到的,而FIN_WAIT_2状态还有时经常能够用netstat看到。 (主动方)
FIN_WAIT_2:上面已经详细解释了这种状态,实际上FIN_WAIT_2状态下的SOCKET,表示半链接,也即 有一方要求close链接,但另外还告诉对方,我暂时还有点数据须要传送给你(ACK信息),稍后再关闭链接。 (主动方)
CLOSE_WAIT:这种状态的含义实际上是表示在等待关闭。怎么理解呢?当对方close一个SOCKET后发送FIN 报文给本身,你系统毫无疑问地会回应一个ACK报文给对方,此时则进入到CLOSE_WAIT状态。接下来呢,实 际上你真正须要考虑的事情是察看你是否还有数据发送给对方,若是没有的话,那么你也就能够 close这个 SOCKET,发送FIN报文给对方,也即关闭链接。因此你在CLOSE_WAIT状态下,须要完成的事情是等待你去关 闭链接。(被动方)
LAST_ACK: 这个状态仍是比较容易好理解的,它是被动关闭一方在发送FIN报文后,最后等待对方的ACK报 文。当收到ACK报文后,也便可以进入到CLOSED可用状态了。(被动方)
TIME_WAIT: 表示收到了对方的FIN报文,并发送出了ACK报文,就等2MSL后便可回到CLOSED可用状态了。 若是FINWAIT1状态下,收到了对方同时带FIN标志和ACK标志的报文时,能够直接进入到TIME_WAIT状态,而无 须通过FIN_WAIT_2状态。(主动方)
CLOSED: 表示链接中断。
7、TCP套接字实践
服务端:
1 from socket import * 2 3 #对数据进行独立化方便更改 4 IP_PORT = ("127.0.0.1",8080) 5 BACK_LOG = 5 6 BUFFER_SIZE = 1024 7 8 #创建TCP链接 9 sever = socket(AF_INET,SOCK_STREAM) #生成套接字对象 10 sever.bind(IP_PORT) #绑定端口 11 sever.listen(BACK_LOG) #开始监听(BACK_LOG:表明的是容许监听的数量) 12 conn,addr = sever.accept() 13 14 #链接成功后的数据传输 15 data = conn.recv(BUFFER_SIZE) 16 de_data = data.decode("utf-8") 17 conn.send(de_data.upper().encode("utf-8")) 18 19 #测试数据发送成功 20 print("send success") 21 22 #关闭链接 23 conn.close() 24 sever.close()
客户端:
1 from socket import * 2 3 IP_PORT = ("127.0.0.1",8080) 4 BUFFER_SIZE = 1024 5 6 #创建链接 7 client = socket(AF_INET,SOCK_STREAM) #生成套接字对象 8 client.connect(IP_PORT) #链接到对应的端口 9 10 #数据传输 11 data = input(">>:") 12 client.send(data.encode("utf-8")) 13 sever_data= client.recv(BUFFER_SIZE) 14 print("success recv:" ,sever_data.decode("utf-8"))
上面的代码就实现了一个简单的基于TCP的套接字服务,可是这仅仅进行了一次简单的通讯,这与电话通讯的你一句我一句有较大的差别。接下来是socket的进阶版:
socket_sever:
1 from socket import * 2 3 #对数据进行独立化方便更改 4 IP_PORT = ("127.0.0.1",8080) 5 BACK_LOG = 5 6 BUFFER_SIZE = 1024 7 8 sever = socket(AF_INET,SOCK_STREAM) 9 sever.bind(IP_PORT) 10 sever.listen(BACK_LOG) 11 conn,addr = sever.accept() 12 13 try: 14 while True: 15 data = conn.recv(BUFFER_SIZE) 16 print("收到数据:",data.decode("utf-8")) 17 deal_data = data.decode("utf-8").upper() 18 conn.send(deal_data.encode("utf-8")) 19 print("发送数据:",deal_data) 20 except ConnectionResetError as e: 21 print(e) 22 23 sever.close() 24
socket_client:
1 from socket import * 2 3 #对数据进行独立化方便更改 4 IP_PORT = ("127.0.0.1",8080) 5 BUFFER_SIZE = 1024 6 7 client = socket(AF_INET,SOCK_STREAM) 8 client.connect(IP_PORT) 9 10 while True: 11 data = input(">>:") 12 if not data:continue 13 client.send(data.encode("utf-8")) 14 rv_data = client.recv(BUFFER_SIZE) 15 print(rv_data.decode("utf-8")) 16 17 client.close()
上述的C/S实现了一个简单的能够无限收发的简单功能,可是此时的socket_sever仅实现了一对一的链接,咱们打点电话的时候还可能有更多的链接进来,而上述的功能在第一 链接中断的时候,第二个服务端也会被中断,因此咱们对上述功能再次进行了加工:
tcp_socket_sever:
1 from socket import * 2 3 #对数据进行独立化方便更改 4 IP_PORT = ("127.0.0.1",8080) 5 BACK_LOG = 5 6 BUFFER_SIZE = 1024 7 8 sever = socket(AF_INET,SOCK_STREAM) 9 sever.bind(IP_PORT) 10 sever.listen(BACK_LOG) 11 12 while True: #这个循环是为了能够接收多个循环,可是每次只能有一个循环进入链接 13 conn, addr = sever.accept() 14 while True: #这个循环是为了可使客户端和服务端循环无限次 15 try: #这是为了解决一个客户端不正常断开的时候而抛出的异常 16 data = conn.recv(BUFFER_SIZE) 17 print("收到数据:",data.decode("utf-8")) 18 deal_data = data.decode("utf-8").upper() 19 conn.send(deal_data.encode("utf-8")) 20 print("已发送数据:",deal_data) 21 except ConnectionResetError as e: 22 print(e) 23 break
tcp_socket_client:
1 from socket import * 2 3 IP_PORT = ("127.0.0.1",8080) 4 BUFFER_SIZE = 1024 5 6 client = socket(AF_INET,SOCK_STREAM) 7 client.connect(IP_PORT) 8 9 while True: 10 data = input(">>:") 11 if not data:continue 12 client.send(data.encode("utf-8")) 13 print("已发送:",data) 14 re_data = client.recv(BUFFER_SIZE) 15 print("已接收:",re_data.decode("utf-8"))
接下来运用socket写一个简单的C/S,做用是能够远程的容许DOS命令,而且返回一些信息,可是没法作更改删除等,仅仅查询功能:
1 from socket import * 2 import subprocess 3 import struct 4 IP_PORT = ("localhost",8080) 5 BACK_LOG = 5 6 BUFFER_SIZE = 1024 7 8 #创建链接 9 sever = socket(AF_INET,SOCK_STREAM) 10 sever.bind(IP_PORT) 11 sever.listen(BACK_LOG) 12 13 while True: 14 conn,addr = sever.accept() 15 print("我准备接收连接了:") 16 while True: 17 try: 18 cmd = conn.recv(BUFFER_SIZE) 19 print("接收到指令:",cmd.decode("utf-8")) 20 21 #对接收到的数据进行处理 22 if not cmd:break #这里的判断解决客户端正常断开时退出链接 23 res = subprocess.Popen( 24 cmd.decode("utf-8"),shell=True, 25 stderr=subprocess.PIPE, 26 stdin=subprocess.PIPE, 27 stdout=subprocess.PIPE 28 ) 29 ''' 30 subprocess模块:shell=True指的是容许将输出在shell的内容输入到管道 31 stderr,stdin,stdout:这些都是将输入流,输出流接入管道 32 ''' 33 err = res.stderr.read() 34 if err: 35 cmd_res = err 36 else: 37 cmd_res = res.stdout.read() 38 39 #发 40 if not cmd_res: 41 cmd_res = "执行成功".encode("gbk") 42 43 conn.send(cmd_res) 44 except Exception as e: 45 print(e) 46 break
1 from socket import * 2 3 IP_PORT = ("localhost",8080) 4 BUFFER_SIZE = 1024 5 6 client = socket(AF_INET,SOCK_STREAM) 7 client.connect(IP_PORT) 8 9 while True: 10 cmd = input(">>:") 11 print("") 12 if not cmd:continue 13 if cmd == "quit":break 14 client.send(cmd.encode("utf-8")) 15 print("已经发送指令:",cmd) 16 data = client.recv(BUFFER_SIZE) 17 print(data.decode("gbk")) 18 19 client.close()
上述的简易的C/S服务还有一个重要的问题没有解决,那就是粘包。
8、粘包问题
在前面的C/S服务咱们输入ipconfig发送到服务端,接收返回到的消息时,会发现接收到的消息不全,再次输入其余命令,接收到的消息仍是原来ipconfig输出的信息,这就的典型的一种粘包现象。
这是为何呢,为何我一个收,一个发为何会出现粘包现象,这就要从底层提及。
根据上图咱们模拟一下简单的TCP套接字工做原理:
第一:启动服务端和客户端
第二:客户端的将指令和请求等数据信息读取到用户态内存,而后内核态将用户态的数据拷贝
第三:而后经过网卡等硬件层发送到服务端的内核态内存
第四:服务端的用户态再从内核态拷贝而后对拷贝过来的数据进行处理
第五:再按照刚才来的路线进行返回。
从上面的流程结合TCP套接字的实例能够得出如下结论:
粘包解决方案一:
1 from socket import * 2 import subprocess 3 import struct 4 IP_PORT = ("localhost",8080) 5 BACK_LOG = 5 6 BUFFER_SIZE = 1024 7 8 sever = socket(AF_INET,SOCK_STREAM) 9 sever.bind(IP_PORT) 10 sever.listen(BACK_LOG) 11 12 while True: 13 conn, addr = sever.accept() 14 print("接收到链接:", addr) 15 while True: 16 try: 17 print("开始接收") 18 cmd = conn.recv(BUFFER_SIZE) 19 print("接收到指令:",cmd.decode("utf-8")) 20 if not cmd:break 21 res = subprocess.Popen( 22 cmd.decode("utf-8"),shell=True, 23 stdout=subprocess.PIPE, 24 stdin=subprocess.PIPE, 25 stderr=subprocess.PIPE 26 ) 27 28 res_err = res.stderr.read() 29 if res_err: 30 res_cmd=res_err 31 else: 32 res_cmd = res.stdout.read() 33 34 data_len = len(res_cmd) 35 print(data_len) 36 conn.send(str(data_len).encode("gbk")) #发送长度 37 data2 = conn.recv(BUFFER_SIZE).decode("gbk") #为了不长度与数据的粘包,先接收一个确认数据 38 #开始发送数据 39 if data2 == "recv_ready": 40 conn.sendall(res_cmd) #这段代码会循环发送,直到数据被发送完毕 41 print("发送完毕") 42 except Exception as e: 43 print(e) 44 break
1 from socket import * 2 3 IP_PORT = ("localhost",8080) 4 BUFFER_SIZE = 1024 5 6 client = socket(AF_INET,SOCK_STREAM) 7 client.connect(IP_PORT) 8 9 ''' 10 解决粘包的方法一就是服务端在发送数据的时候,先发一个数据长度,而后让客户端循环收,直到收完为止 11 ''' 12 13 while True: 14 cmd = input(">>:") 15 if not cmd:continue 16 if cmd == "quit":break 17 18 client.send(cmd.encode("utf-8")) #发送命令 19 cmd_data_lenth = int(client.recv(BUFFER_SIZE).decode("gbk")) #接收长度 20 print(cmd_data_lenth) 21 client.send("recv_ready".encode("utf-8")) 22 cmd_data = "" 23 data_lenth= 0 24 while data_lenth < cmd_data_lenth: 25 cmd_data += client.recv(BUFFER_SIZE).decode("gbk") 26 print(cmd_data) 27 data_lenth += len(cmd_data) 28 print("数据接收完毕",cmd_data) 29 30 client.close()
该方案程序的运行速度远快于网络传输速度,因此在发送一段字节前,先用send去发送该字节流长度,这种方式会放大网络延迟带来的性能损耗
粘包解决方案二: