软件开发架构 开发软件,必须要开发一套 客户端 和 服务端
服务端与客户端的作用
服务端:24小时不间断提供服务
客户端:享受服务
C/S架构 Client:客户端
server:服务端
优点:
缺点:
若用户想要在同一设备使用多个软件,必须下载多个客户端。
软件的么一次更新,用户是必须也跟着重新下载更新
C/S架构的软件:
PC端的QQ,移动端的微信、qq等
数据放在服务端和客户端的利弊:
服务端同意处理有更好的安全性和稳定性而且升级比较容易,不过服务器 负担就增加了
客户端将负担分配到每个用户,从而节约服务器资源,安全性和稳定性可能会有问题,但是升级比较麻烦,每个安装的客户端程序都需要升级,另外为了节省网络资源,通过网络传输的数据也应该减少!
B/S架构 Browser:浏览器
Server:服务端
优点:
以浏览器充当客户端,无需用户下载多个软件,也无需用户下载更新软件,直接在浏览器中访问需要的软件
缺点:
消耗网络资源过大,当网络不稳定时,软件的使用也不稳定
应用领域: 淘宝、京东
OSI七层协议 实现远程通信具备:
物理连接介质——> 网卡
互联网协议:计算机之间沟通的介质“互联网协议”
互联网的本质就是一系列的网络协议,这个协议就是osi。(OSI是Open System Interconnection的缩写)。按照分工的不同,人为划分为七层,
从下往上分别是物理层,数据链路层,网络层,传输层,会话层,表示层,应用层。
物理层 物理层:物理传输,在不同地方的电脑想要通信必须,电脑要插一根网线、路由器,才可以实现。也就是说要有物理介质连接,
中间的物理链接可以是光缆、电缆、双绞线、无线电波 ,基于电信号发送二进制数据0100010101。
但是一堆二进制数据没有分组,不知道什么意思,这就要交给数据链路层处理。
数据链路层 数据链路层的“以太网协议”, 专门用于处理基于电信号发送二进制的数据。
以太网协议:
规定好电信号数据的分组方式
每一台连接网线的电脑都必须要有一块网卡,发送端的和接收端的地址就是网卡的地址,即mac地址
mac地址 网卡由不同厂商生产,每块网卡出厂都会被烧录上实际上唯一的mac地址,由48位二进制组成,通常用12位16进制表示,前6位是厂商编号,后6位是流水线号
有了mac地址计算机就可以通信了。假设一个教室就是一个局域网,教室里有几台计算机,计算机的通信就使用广播的方式,靠吼。
比如说我想找老张要片,我得知道自己的mac地址和老张的mac地址,这两个地址做数据包的头部,再加上数据片就构成了一个数据包。
这样发送出去,然后就通过广播的方式,在教室的所有人都听到了,都会拆这个数据包,看是找谁的,只要不是找自己的就丢掉,其实就是再看想找那个mac地址,找的是老张,这样老张就会收到消息并把片发给我。这就是局域网通信。
但我有事找老王,他在隔壁教室,这样他就听不到我广播了,这就得交给网络层来处理了。
网络层 IP协议 有两种版本,一个是IPv4,另一个是IPv6
IPv4是由32位二进制组成,32位分成4组,8位一组换算成10进制显示,采用点分制。范围从0.0.0.0
到255.255.255.255
通过IP协议可以跨局域网通信。我想找隔壁教室的老王,我在自己教师广播,他肯定是听不到的。那么我就得找教室负责人,把东西交给负责人,让他去找隔壁教室负责人,说我想找你教室的老王,然后把东西交给负责人,让负责人带话给老王。负责人就是网关。
Mac地址是用来标识你这个教室的某个位置,IP地址是用来标识你在哪个教室(哪个局域网)。你要跨网络发包你是不是要知道对方的IP地址,比如你要访问百度,你肯定得知道百度服务器的IP地址。计算机在发包前,会判断你在哪个教室,对方在哪个教室,如果在一个教室,基于mac地址的广播发包就OK了;如果不在一个教室,即跨网络发包,那么就会把你的包交给教室负责人(网关)来转发。Mac地址及IP地址唯一标识了你在互联网中的位置。
ARP协议 IP地址与mac地址的映射关系
传输层 传输层建立端口到端口的通信
端口标识主机上的应用程序,是应用程序与网卡关联的编号
端口范围:0-65535,前1024个端口为系统占用
有了IP地址+mac地址+端口,我们就能确定世界上唯一的计算机的应用程序
TCP协议
可靠传输,提供的是流式协议为了方便传输, 将大块数据分割成以报文段(segment) 为单位的数据包进行管理。 而可靠的传输服务是指, 能够把数据准确可靠地传给对方。 即TCP 协议为了更容易传送大数据才把数据分割, 而且 TCP 协议能够确认数据最终是否送达到对方。所以,TCP连接相当于两根管道(一个用于服务器到客户端,一个用于客户端到服务器),管道里面数据传输是通过字节码传输,传输是有序的,每个字节都是一个一个来传输。
UDP协议 无连接协议,不可靠传输
应用层 用户使用的程序都是应用程序,均工作于应用层,规定了应用程序的数据格式
TCP三次握手,四次挥手 三次挥手
第一次握手 :客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN(c)。此时客户端处于 SYN_SEND
状态。
首部的同步位SYN=1,初始序号seq=x,SYN=1的报文段不能携带数据,但 要消耗掉一个序号。
第二次握手 :服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN(s)。同时会把客户端的
ISN + 1 作为ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_REVD
的状态。
在确认报文段中SYN=1,ACK=1,确认号ack=x+1,初始序号seq=y。
第三次握手 :客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLISHED
状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED
状态,此时,双方已建立起了连接。
确认报文段ACK=1,确认号ack=y+1,序号seq=x+1(初始为seq=x,第二个报文段所以要+1),ACK报文段可以携带数据,不携带数据则不消耗序号。
四次挥手
建立一个连接需要三次握手,而终止一个连接要经过四次挥手(也有将四次挥手叫做四次握手的)。这由TCP的半关闭 (half-close)造成的。所谓的半关闭,其实就是TCP提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力。
TCP 的连接的拆除需要发送四个包,因此称为四次挥手(Four-way handshake),客户端或服务器均可主动发起挥手动作。
刚开始双方都处于 ESTABLISHED 状态,假如是客户端先发起关闭请求。四次挥手的过程如下:
第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于 FIN_WAIT1
状态。
即发出连接释放报文段 (FIN=1,序号seq=u),并停止再发送数据,主动关闭TCP连接,进入FIN_WAIT1(终止等待1)状态,等待服务端的确认。
第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 +1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT
状态。
即服务端收到连接释放报文段后即发出确认报文段 (ACK=1,确认号ack=u+1,序号seq=v),服务端进入CLOSEWAIT(关闭等待)状态,此时的TCP处于半关闭状态,客户端到服务端的连接释放。客户端收到服务端的确认后,进入FINWAIT2(终止等待2)状态,等待服务端发出的连接释放报文段。
第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK
的状态。
即服务端没有要向客户端发出的数据,服务端发出连接释放报文段 (FIN=1,ACK=1,序号seq=w,确认号ack=u+1),服务端进入LAST_ACK(最后确认)状态,等待客户端的确认。
第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 +1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT
状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态,服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED
状态。
即客户端收到服务端的连接释放报文段后,对此发出确认报文段 (ACK=1,seq=u+1,ack=w+1),客户端进入TIME_WAIT(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL后,客户端才进入CLOSED状态。
收到一个FIN只意味着在这一方向上没有数据流动。客户端执行主动关闭并进入TIMEWAIT是正常的,服务端通常执行被动关闭,不会进入TIMEWAIT状态。
什么是socket Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。
基于TCP协议的套接字编程(简单) 服务端 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import socketserver = socket.socket() server.bind( ('127.0.0.1' , 9999 ) ) server.listen(5 ) conn, addr = server.accept() print (addr)data = conn.recv(1024 ).decode('utf-8' ) print (data)conn.send('来自服务端消息:我不好' .encode('utf-8' )) conn.close() server.close()
客户端 1 2 3 4 5 6 7 8 9 10 11 12 13 14 import socketclient = socket.socket() client.connect( ('127.0.0.1' , 9999 ) ) client.send('来自客户端消息:你好' .encode('utf-8' )) data = client.recv(1024 ).decode('utf-8' ) print (data)client.close()
基于TCP协议的套接字编程(复杂) 服务端 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import socketserver = socket.socket() server.bind( ('127.0.0.1' , 9999 ) ) server.listen(5 ) conn, addr = server.accept() print (addr)while True : data = conn.recv(1024 ).decode('utf-8' ) print (data) if data == 'q' : break send_msg = input ('server--->client: ' ).encode('utf-8' ) conn.send(send_msg) conn.close() server.close()
客户端 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import socketclient = socket.socket() client.connect( ('127.0.0.1' , 9999 ) ) while True : send_msg = input ('client---> server:' ) client.send(send_msg.encode('utf-8' )) if send_msg == 'q' : break data = client.recv(1024 ).decode('utf-8' ) print (data) client.close()
服务端服务多个客户 服务端 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import socketserver = socket.socket() server.bind( ('127.0.0.1' , 8888 ) ) server.listen(5 ) while True : conn, addr = server.accept() print (addr) while True : try : data = conn.recv(1024 ).decode('utf-8' ) print (data) if len (data) == 0 : continue if data == 'q' : break send_msg = input ('server--->client:' ).encode('utf-8' ) conn.send(send_msg) except Exception as e: print (e) break conn.close()
客户端 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import socketclient = socket.socket() client.connect( ('127.0.0.1' , 8888 ) ) while True : send_msg = input ('client--->server:' ) client.send(send_msg.encode('utf-8' )) if send_msg == 'q' : break data = client.recv(1024 ).decode('utf-8' ) print (data) client.close()
TCP粘包问题 服务端第一次发送的数据,客户端无法精确一次性接收完毕,下一次发送的数据与上一次数据黏在一起了。
无法预测对方需要接收的数据大小长度
TCP流式协议,会将多次连续发送数据量小、并且时间间隔短的数据一次性打包发送。
基于TCP的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始何处结束。
粘包两种情况
服务端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import socketserver = socket.socket() server.bind( ('127.0.0.1' , 9527 ) ) server.listen(5 ) conn, addr = server.accept() data1 = conn.recv(1024 ) data2 = conn.recv(1024 ) data3 = conn.recv(1024 ) print (data1)print (data2)print (data3)import socketclient = socket.socket() client.connect( ('127.0.0.1' , 9527 ) ) client.send(b'hello' ) client.send(b'hello' ) client.send(b'hello' ) client.close() ''' b'hellohellohello' b'' b'' '''
客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import socketserver = socket.socket() server.bind( ('127.0.0.1' , 9527 ) ) server.listen(5 ) conn, addr = server.accept() data1 = conn.recv(2 ) data2 = conn.recv(20 ) print (data1)print (data2)import socketclient = socket.socket() client.connect( ('127.0.0.1' , 9527 ) ) client.send(b'hello world and shcaondskasd nknasksfn km' ) client.close() ''' b'he' b'llo world and shcaon '''
解决粘包问题 使用struct模块
struct模块是一个可以将很长的数据的长度,压缩成固定的长度的一个标记(数据报头)。
必须先定义报头,发送报头,再发送真实数据
struct模块的使用 1 2 3 4 5 6 7 8 9 10 11 12 import structstr1 = '123a56' headers = struct.pack('i' , len (str1)) print (headers)print (len (headers))data_len = struct.unpack('i' , headers) print (data_len[0 ])
使用struct模块解决粘包 服务端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import structimport socketimport subprocessserver = socket.socket() server.bind( ('127.0.0.1' , 8888 ) ) server.listen(5 ) while True : conn, addr = server.accept() print (addr) while True : try : cmd = conn.recv(1024 ).decode('utf-8' ) if len (cmd) == 0 : continue if cmd == 'q' : break res = subprocess.Popen( cmd, shell=True , stdout=subprocess.PIPE, stderr=subprocess.PIPE) data = res.stdout.read() + res.stderr.read() headers = struct.pack('i' , len (data)) conn.send(headers) conn.send(data) except Exception: break conn.close()
客户端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import structimport socketclient = socket.socket() client.connect( ('127.0.0.1' , 8888 ) ) while True : cmd = input ('cmd>>>:' ) client.send(cmd.encode('utf-8' )) if cmd == 'q' : break headers = client.recv(4 ) data_len = struct.unpack('i' , headers)[0 ] data = client.recv(data_len) print (data.decode('gbk' )) client.close()
优化解决粘包问题 把数据真实长度和数据的描述信息一同发送过去
服务端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import socketimport structimport jsonserver = socket.socket() server.bind(('127.0.0.1' , 9999 )) server.listen(5 ) while True : conn, addr = server.accept() print (addr) while True : try : headers = conn.recv(4 ) data_len = struct.unpack('i' , headers)[0 ] bytes_data = conn.recv(data_len) back_dic = json.loads(bytes_data.decode('utf-8' )) print (back_dic) except Exception: break conn.close()
客户端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import socketimport structimport jsonclient = socket.socket() client.connect(('127.0.0.1' , 9999 )) while True : dic = { 'file_name' : 'shoot on me' , 'file_size' : 10000000 } json_data = json.dumps(dic) json_bytes = json_data.encode('utf-8' ) headers = struct.pack('i' , len (json_bytes)) client.send(headers) client.send(json_bytes)
上传大文件 服务端 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import socketimport structimport jsonserver = socket.socket() server.bind(('127.0.0.1' , 9999 )) server.listen(5 ) conn, addr = server.accept() print (addr)while True : try : headers = conn.recv(4 ) data_len = struct.unpack('i' , headers)[0 ] bytes_data = conn.recv(data_len) back_dic = json.loads(bytes_data.decode('utf-8' )) print (back_dic) file_name = back_dic.get('file_name' ) file_size = back_dic.get('file_size' ) init_data = 0 with open (file_name, 'wb' ) as f: while init_data < file_size: data = conn.recv(1024 ) f.write(data) init_data += len (data) print (f'{file_name} 接收完毕! ' ) except Exception as e: print (e) break conn.close()
客户端 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import socketimport structimport jsonclient = socket.socket() client.connect(('127.0.0.1' , 9999 )) with open (r'D:\pycharm_project\忌日快乐2.mkv' , 'rb' ) as f: movie_bytes = f.read() send_dic = { 'file_name' : '忌日快乐2.mkv' , 'file_size' : len (movie_bytes) } json_data = json.dumps(send_dic) bytes_data = json_data.encode('utf-8' ) headers = struct.pack('i' , len (bytes_data)) client.send(headers) client.send(bytes_data) init_data = 0 num = 1 with open (r'D:\pycharm_project\忌日快乐2.mkv' , 'rb' ) as f: while init_data < len (movie_bytes): send_data = f.read(1024 ) print (send_data, num) num += 1 client.send(send_data) init_data += len (send_data)
UDP协议 UDP是一种传输协议
不需要建立双向通道
不会粘包
客户端给服务端发送数据,不需要等待服务返回接收成功
UDP套接字虽然没有粘包问题,但是不能替代TCP套接字,因为UPD协议有一个缺陷:如果数据发送的途中,数据丢失,则数据就丢失了,而TCP协议则不会有这种缺陷,因此一般UPD套接字用户无关紧要的数据发送,例如qq聊天。
UDP套接字 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import socketserver = socket.socket(type =socket.SOCK_DGRAM) server.bind( ('127.0.0.1' , 8888 ) ) msg, addr = server.recvfrom(1024 ) msg1, addr1 = server.recvfrom(1024 ) msg2, addr2 = server.recvfrom(1024 ) msg3, addr3= server.recvfrom(1024 ) print (msg)print (msg1)print (msg2)print (msg3)import socketclient = socket.socket(type =socket.SOCK_DGRAM) client.sendto(b'hello' , ('127.0.0.1' , 8888 )) client.sendto(b'hello' , ('127.0.0.1' , 8888 )) client.sendto(b'hello' , ('127.0.0.1' , 8888 )) client.sendto(b'hello' , ('127.0.0.1' , 8888 )) ''' b'hello' b'hello' b'hello' b'hello' '''
基于UDP实现qq聊天室 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import socketserver = socket.socket(type =socket.SOCK_DGRAM) server.bind(('127.0.0.1' , 8888 )) while True : msg, addr = server.recvfrom(1024 ) print (addr) print (msg.decode('utf-8' )) send_msg = input ('服务端发送消息:' ).encode('utf-8' ) server.sendto(send_msg, addr) import socketclient = socket.socket(type =socket.SOCK_DGRAM) server_ip_port = ('127.0.0.1' , 8888 ) while True : send_msg = input ('客户端1:' ).encode('utf-8' ) client.sendto(send_msg, server_ip_port) msg, addr = client.recvfrom(1024 ) print (msg.decode('utf-8' ))
socketserver python内置模块,可以简化socket套接字服务端的代码,必须要创建一个类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 import socketserverclass MyTCPServer (socketserver.BaseRequestHandler): def handle (self ): print (self.client_address) while True : try : data = self.request.recv(1024 ).decode('utf-8' ) send_msg = data.upper() self.request.send(send_msg.encode('utf-8' )) except Exception: break if __name__ == '__main__' : server = socketserver.ThreadingTCPServer ( ('127.0.0.1' , 8888 ), MyTCPServer ) server.serve_forever() import socketclient = socket.socket() client.connect( ('127.0.0.1' , 8888 ) ) while True : send_msg = input ('客户端:' ) if send_msg == 'q' : break client.send(send_msg.encode('utf-8' )) data = client.recv(1024 ).decode('utf-8' ) print (data) client.close()