Python基础_网络

Python基础_网络 网络编程是指在计算机网络上进行数据传输和通信的编程技术。它涉及到在不同计算机之间建立连接、发送和接收数据以及处理网络通信的各种操作。网络编程广泛应用于各种领域包括服务器开发、Web开发、分布式系统、云计算等。1. 软件架构设计软件架构设计可以根据应用场景的不同分为客户端/服务器Client/ServerCS架构和浏览器/服务器Browser/ServerBS架构。【1】CS架构CS架构是一种常见的软件架构它将软件系统划分为两个主要部分客户端和服务器。客户端负责展示用户界面和处理用户输入而服务器负责处理业务逻辑和存储数据。客户端和服务器之间通过网络进行通信客户端发送请求给服务器服务器进行处理并返回结果给客户端。在CS架构中客户端和服务器可以运行在不同的物理设备上通过网络连接进行通信。客户端可以是桌面应用程序、移动应用程序等而服务器可以是独立的物理服务器或云服务器。例如你电脑的上QQ、百度网盘、钉钉、QQ音乐 等安装在电脑上的软件。服务端互联网公司会开发一个程序放在他们的服务器上用于给客户端提供数据支持。客户端大家在电脑安装的相关程序内部会连接服务端进行收发数据并提供 交互和展示的功能。【2】BS架构BS架构是一种特殊的CS架构其中客户端是通过浏览器访问应用程序而服务器负责提供应用程序的逻辑和数据。在BS架构中客户端使用浏览器作为用户界面通过HTTP协议与服务器进行通信。BS架构的优势在于客户端无需安装额外的软件只需使用普通的浏览器即可访问应用程序。这使得应用程序的部署和维护更加方便同时也提供了跨平台和跨设备的能力。在BS架构中服务器端主要负责业务逻辑和数据处理而客户端主要负责展示和用户交互。服务器端可以使用不同的技术栈如Web服务器、应用服务器、数据库服务器等。例如淘宝、京东等网站。服务端互联网公司开发一个网站放在他们的服务器上。 客户端不需要开发用现成的浏览器即可。需要注意的是CS架构和BS架构并不是互斥的而是根据应用场景和需求选择最适合的架构。某些应用可能需要同时使用CS和BS架构例如在一个企业内部系统中可以使用CS架构的桌面应用程序和BS架构的Web应用程序相结合以满足不同的用户需求和使用场景。2. 网络三要素地址Address地址用于唯一标识网络中的设备或应用程序。在网络通信中每个设备或应用程序都有一个唯一的地址使得数据能够准确地发送到目标位置。在Internet中常用的地址是IP地址Internet Protocol Address它是一个由数字和点分隔符组成的标识符。IP地址可以用来标识主机计算机或网络设备。此外还有其他类型的地址如MAC地址Media Access Control Address用于在局域网中唯一标识网络接口。端口Port端口是在网络通信中用于标识应用程序或服务的数字。每个设备或主机上的应用程序可以使用不同的端口号以便在同一台设备上同时运行多个应用程序。端口号是一个16位的数字范围从0到65535。其中0到1023之间的端口号是一些著名的端口用于特定的服务或应用程序如HTTP的端口号是80HTTPS的端口号是443。端口号的使用确保了数据能够正确地传递给目标应用程序或服务。协议Protocol协议是在网络通信中规定的一组规则和约定用于确保数据的正确传输和交换。协议定义了数据的格式、传输方式、错误处理、连接建立和断开等操作。常见的网络协议包括TCP传输控制协议、UDP用户数据报协议、IP互联网协议、HTTP超文本传输协议等。协议的使用确保了网络中的设备和应用程序之间可以相互通信和理解。3. TCP协议和UDP协议【1】TCP协议TCPTransmission Control Protocol传输控制协议是一种面向连接的、可靠的、基于字节流的通信协议数据在传输前要建立连接传输完毕后还要断开连接。客户端在收发数据前要使用 connect() 函数和服务器建立连接。建立连接的目的是保证IP地址、端口、物理链路等正确无误为数据的传输开辟通道。序号SeqSequence Number序号占32位用来标识从计算机A发送到计算机B的数据包的序号计算机发送数据时对此进行标记。确认号AckNumber确认号占32位客户端和服务器端都可以发送Ack Seq 1。标志位每个标志位占用1Bit共有6个分别为 URG、ACK、PSH、RST、SYN、FIN具体含义如下// URG紧急指针urgent pointer有效。// ACK确认序号有效。// PSH接收方应该尽快将这个报文交给应用层。// RST重置连接。// SYN建立一个新连接。// FIN断开一个连接。TCP建立连接时要传输三个数据包俗称三次握手Three-way Handshaking。可以形象的比喻为下面的对话[Shake 1] 套接字A“大哥你能听到我说话吗”[Shake 2] 套接字B“可以小弟你能听到我说话吗”[Shake 3] 套接字A“我也能OK”用 connect() 建立连接时客户端和服务器端会相互发送三个数据包请看下图客户端调用 socket() 创建套接字后因为没有建立连接所以套接字处于CLOSED状态服务器端调用 listen() 函数后套接字进入LISTEN状态开始监听客户端请求。这个时候客户端开始发起请求当客户端调用 connect() 函数后TCP协议会组建一个数据包并设置 SYN 标志位表示该数据包是用来建立同步连接的。同时生成一个随机数字 1000填充“序号Seq”字段表示该数据包的序号。完成这些工作开始向服务器端发送数据包客户端就进入了SYN-SEND状态。服务器端收到数据包检测到已经设置了 SYN 标志位就知道这是客户端发来的建立连接的“请求包”。服务器端也会组建一个数据包并设置 SYN 和 ACK 标志位SYN 表示该数据包用来建立连接ACK 用来确认收到了刚才客户端发送的数据包。 服务器生成一个随机数 2000填充“序号Seq”字段。2000 和客户端数据包没有关系。服务器将客户端数据包序号1000加1得到1001并用这个数字填充“确认号Ack”字段。服务器将数据包发出进入SYN-RECV状态。客户端收到数据包检测到已经设置了 SYN 和 ACK 标志位就知道这是服务器发来的“确认包”。客户端会检测“确认号Ack”字段看它的值是否为 10001如果是就说明连接建立成功。接下来客户端会继续组建数据包并设置 ACK 标志位表示客户端正确接收了服务器发来的“确认包”。同时将刚才服务器发来的数据包序号2000加1得到 2001并用这个数字来填充“确认Ack”字段。客户端将数据包发出进入ESTABLISED状态表示连接已经成功建立。服务器端收到数据包检测到已经设置了 ACK 标志位就知道这是客户端发来的“确认包”。服务器会检测“确认号Ack”字段看它的值是否为 20001如果是就说明连接建立成功服务器进入ESTABLISED状态。至此客户端和服务器都进入了ESTABLISED状态连接建立成功接下来就可以收发数据了。注意三次握手的关键是要确认对方收到了自己的数据包这个目标就是通过“确认号Ack”字段实现的。计算机会记录下自己发送的数据包序号 Seq待收到对方的数据包后检测“确认号Ack”字段看Ack Seq 1是否成立如果成立说明对方正确收到了自己的数据包【2】UDP协议TCP 是面向连接的传输协议建立连接时要经过三次握手断开连接时要经过四次握手中间传输数据时也要回复 ACK 包确认多种机制保证了数据能够正确到达不会丢失或出错。UDP 是非连接的传输协议没有建立连接和断开连接的过程它只是简单地把数据丢到网络中也不需要 ACK 包确认。UDP 传输数据就好像我们邮寄包裹邮寄前需要填好寄件人和收件人地址之后送到快递公司即可但包裹是否正确送达、是否损坏我们无法得知也无法保证。UDP 协议也是如此它只管把数据包发送到网络然后就不管了如果数据丢失或损坏发送端是无法知道的当然也不会重发。既然如此TCP 应该是更加优质的传输协议吧如果只考虑可靠性TCP 的确比 UDP 好。但 UDP 在结构上比 TCP 更加简洁不会发送 ACK 的应答消息也不会给数据包分配 Seq 序号所以 UDP 的传输效率有时会比 TCP 高出很多编程中实现 UDP 也比 TCP 简单。UDP 的可靠性虽然比不上TCP但也不会像想象中那么频繁地发生数据损毁在更加重视传输效率而非可靠性的情况下UDP 是一种很好的选择。比如视频通信或音频通信就非常适合采用 UDP 协议通信时数据必须高效传输才不会产生“卡顿”现象用户体验才更加流畅如果丢失几个数据包视频画面可能会出现“雪花”音频可能会夹带一些杂音这些都是无妨的。与 UDP 相比TCP 的生命在于流控制这保证了数据传输的正确性。4. Socket套接字【1】socket 概念socket 的原意是“插座”在计算机通信领域它是计算机之间进行通信的一种约定或一种方式。通过 socket 这种约定一台计算机可以接收其他计算机的数据也可以向其他计算机发送数据。 我们把插头插到插座上就能从电网获得电力供应同样为了与远程计算机进行数据传输需要连接到因特网而 socket 就是用来连接到因特网的工具。socket是在应用层与传输层之间的一个抽象层它的本质是编程接口通过socket才能实现TCP/IP协议。它就是一个底层套件用来处理最底层消息的接受和发送。【2】套接字的类型根据数据的传输方式可以将 Internet 套接字分成两种类型。通过 socket() 函数创建连接时必须告诉它使用哪种数据传输方式。1流格式套接字SOCK_STREAM流格式套接字Stream Sockets也叫“面向连接的套接字”在代码中使用 SOCK_STREAM 表示。SOCK_STREAM 是一种可靠的、双向的通信数据流数据可以准确无误地到达另一台计算机如果损坏或丢失可以重新发送。SOCK_STREAM 有以下几个特征数据在传输过程中不会消失数据是按照顺序传输的数据的发送和接收不是同步的有的教程也称“不存在数据边界”。可以将 SOCK_STREAM 比喻成一条传送带只要传送带本身没有问题不会断网就能保证数据不丢失同时较晚传送的数据不会先到达较早传送的数据不会晚到达这就保证了数据是按照顺序传递的。为什么流格式套接字可以达到高质量的数据传输呢这是因为它使用了 TCP 协议The Transmission Control Protocol传输控制协议TCP 协议会控制你的数据按照顺序到达并且没有错误。你也许见过 TCP是因为你经常听说“TCP/IP”。TCP 用来确保数据的正确性IPInternet Protocol网络协议用来控制数据如何从源头到达目的地也就是常说的“路由”。那么“数据的发送和接收不同步”该如何理解呢假设传送带传送的是水果接收者需要凑齐 100 个后才能装袋但是传送带可能把这 100 个水果分批传送比如第一批传送 20 个第二批传送 50 个第三批传送 30 个。接收者不需要和传送带保持同步只要根据自己的节奏来装袋即可不用管传送带传送了几批也不用每到一批就装袋一次可以等到凑够了 100 个水果再装袋。流格式套接字的内部有一个缓冲区也就是字符数组通过 socket 传输的数据将保存到这个缓冲区。接收端在收到数据后并不一定立即读取只要数据不超过缓冲区的容量接收端有可能在缓冲区被填满以后一次性地读取也可能分成好几次读取。也就是说不管数据分几次传送过来接收端只需要根据自己的要求读取不用非得在数据到达时立即读取。传送端有自己的节奏接收端也有自己的节奏它们是不一致的。2数据报套接字SOCK_DGRAM数据报格式套接字Datagram Sockets也叫“无连接的套接字”在代码中使用 SOCK_DGRAM 表示。计算机只管传输数据不作数据校验如果数据在传输中损坏或者没有到达另一台计算机是没有办法补救的。也就是说数据错了就错了无法重传。因为数据报套接字所做的校验工作少所以在传输效率方面比流格式套接字要高。可以将 SOCK_DGRAM 比喻成高速移动的摩托车快递它有以下特征强调快速传输而非传输顺序传输的数据可能丢失也可能损毁限制每次传输的数据大小数据的发送和接收是同步的有的教程也称“存在数据边界”。众所周知速度是快递行业的生命。用摩托车发往同一地点的两件包裹无需保证顺序只要以最快的速度交给客户就行。这种方式存在损坏或丢失的风险而且包裹大小有一定限制。因此想要传递大量包裹就得分配发送。另外用两辆摩托车分别发送两件包裹那么接收者也需要分两次接收所以“数据的发送和接收是同步的”换句话说接收次数应该和发送次数相同。总之数据报套接字是一种不可靠的、不按顺序传递的、以追求速度为目的的套接字。数据报套接字也使用 IP 协议作路由但是它不使用 TCP 协议而是使用 UDP 协议User Datagram Protocol用户数据报协议。QQ 视频聊天和语音聊天就使用 SOCK_DGRAM 来传输数据因为首先要保证通信的效率尽量减小延迟而数据的正确性是次要的即使丢失很小的一部分数据视频和音频也可以正常解析最多出现噪点或杂音不会对通信质量有实质的影响。注意SOCK_DGRAM 没有想象中的糟糕不会频繁的丢失数据数据错误只是小概率事件。【3】基于套接字的网络编程socket翻译为套接字可以把TCP/IP复杂的操作抽象为简单的几个接口来供应用层调用来实现进程在网络中的通信。socket起源于Unix而Unix的基本要素之一就是“一切都为文件”即可以通过打开——读写——关闭的模式来操作通过这一点我们就可以来实现socket的简单编写1服务端importsocketfromloguruimportlogger# (1) 构建服务端套接字对象socksocket.socket(familysocket.AF_INET,typesocket.SOCK_STREAM)# (2) 服务端三件套bind listen acceptsock.bind((127.0.0.1,8899))sock.listen(5)logger.info(服务器启动)while1:logger.info(等待新连接...)conn,addrsock.accept()# 阻塞函数# print(fconn{conn}addr{addr})logger.info(f来自于客户端{addr}的请求成功)while1:# (3) 收消息data_bytesconn.recv(1024)# 阻塞函数print(data:,data_bytes.decode())ifdata_bytesquit.encode()orlen(data_bytes)0:logger.info(f来自于{addr}客户端退出)break# (4) 处理数据并发送datadata_bytes.decode()resdata.upper()conn.send(res.encode())2客户端import socket # (1) 构建客户端套接字对象 sock socket.socket(familysocket.AF_INET, typesocket.SOCK_STREAM) # (2) 连接服务器 sock.connect((127.0.0.1, 8899)) while 1: name input(请输入转换的姓名(英文):) # (3) 发消息: 网络传输的数据一定是字节串 sock.send(name.encode()) # # 客户端退出 if name quit: break # (4) 接受来自于服务的响应消息 res sock.recv(1024) print(来自于服务的响应消息, res.decode())5. 案例练习【1】模拟SSH实现实现了一个基本的命令执行系统允许客户端向服务器发送命令并获取执行结果。服务器接收到客户端的命令后使用subprocess.getoutput()执行命令并将结果发送回客户端。客户端接收服务器返回的结果并进行处理。1服务端importsocketimportsubprocessimporttimeimportstruct socksocket.socket()sock.bind((127.0.0.1,6666))sock.listen(5)while1:conn,addrsock.accept()print(客户端%s建立连接%str(addr))while1:cmdconn.recv(1024)# data字节串ifnotcmd:print(f{conn.getpeername()}客户端退出)breakprint(执行命令,cmd.decode(gbk))# 版本1cmd_ret_bytessubprocess.getoutput(cmd).encode()conn.send(cmd_ret_bytes)# 版本2cmd_ret_bytessubprocess.getoutput(cmd).encode()print(响应字节数,len(cmd_ret_bytes))cmd_res_bytes_lenstr(len(cmd_ret_bytes)).encode()conn.send(cmd_res_bytes_len)conn.send(cmd_res_bytes)2客户端importsocketimporttime ip_port(127.0.0.1,6666)sksocket.socket()sk.connect(ip_port)while1:datainput(输入执行命令)sk.send(data.encode())# 版本1ressk.recv(1024)print(字节长度,len(res))print(执行命令结果:%s%(res.decode()))# 版本2cmd_ret_bytes_lensk.recv(1024)cmd_res_lenint(cmd_ret_bytes_len.decode())recv_num0whilerecv_numcmd_res_len:datask.recv(1024)print(data.decode())recv_numlen(data)print(data的长度,recv_num,cmd_res_len)【2】粘包现象粘包Packet Congestion是计算机网络中的一个常见问题粘包问题通常出现在使用面向连接的传输协议如TCP进行数据传输时这是因为TCP是基于字节流的它并不了解应用层数据包的具体边界。当发送端迅速发送多个数据包时底层网络协议栈可能会将这些数据包合并成一个较大的数据块进行发送。同样地在接收端网络协议栈也可能将接收到的数据块合并成一个较大的数据块然后交给应用层处理。粘包问题可能导致数据处理的困难和不准确性。例如在一个基于文本的协议中接收方可能需要将接收到的数据进行分割以便逐个处理每个完整的消息。如果数据包粘连在一起接收方就需要额外的处理来确定消息的边界这增加了复杂性。demo演示# 服务端importsocketimporttime ssocket.socket()s.bind((127.0.0.1,8888))s.listen(5)client,addrs.accept()time.sleep(10)dataclient.recv(1024)print(data)client.send(data)# 客户端importsocket ssocket.socket()s.connect((127.0.0.1,8888))datainput()s.send(data.encode())s.send(data.encode())s.send(data.encode())ress.recv(1024)print(res)解决SSH案例的粘包服务端importsocketimportsubprocessimporttimeimportstruct socksocket.socket()sock.bind((127.0.0.1,6666))sock.listen(5)while1:conn,addrsock.accept()print(客户端%s建立连接%str(addr))while1:cmdconn.recv(1024)# data字节串ifnotcmd:print(f{conn.getpeername()}客户端退出)breakprint(执行命令,cmd.decode(gbk))# 版本3粘包解决方案result_strsubprocess.getoutput(cmd.decode(gbk))result_bytesbytes(result_str,encodingutf8)res_lenstruct.pack(i,len(result_bytes))print(res_len:,res_len)conn.sendall(res_len)conn.sendall(result_bytes)客户端importsocketimporttimeimportstruct ip_port(127.0.0.1,6666)sksocket.socket()sk.connect(ip_port)while1:datainput(输入执行命令)sk.send(data.encode())# 版本3粘包解决方案time.sleep(5)length_msgsk.recv(4)lengthstruct.unpack(i,length_msg)[0]recv_num0whilerecv_numlength:datask.recv(1024)print(data.decode())recv_numlen(data)print(data的长度,recv_num,length)【3】上传文件客户端importsocketimportosimportjson# (1) 构建套接字对象确定通信协议socksocket.socket(familysocket.AF_INET,typesocket.SOCK_STREAM)ip_port(127.0.0.1,5566)sock.connect(ip_port)while1:# 客户端给服务端发送消息paramsinput(请输入命令(比如上传文件put 文件路径):)cmd,local_pathparams.split( )# (1) 将文件信息传给服务端file_sizeos.path.getsize(local_path)file_nameos.path.basename(local_path)file_params{file_name:file_name,file_size:file_size}sock.send(json.dumps(file_params).encode())# (2) 循环读取文件传给server端withopen(local_path,rb)asf:forlineinf:sock.send(line)print(文件发送完毕)服务端importsocketimportjson# (1) 构建套接字对象确定通信协议socksocket.socket(familysocket.AF_INET,typesocket.SOCK_STREAM)# (2) 绑定IP和端口ip_port(127.0.0.1,5566)sock.bind(ip_port)# (3) 监听最大排队数sock.listen(2)# (4) 阻塞等待客户端连接while1:print(server is waiting...)conn,addrsock.accept()while1:# 接受来自客户端的文件信息data_jsonconn.recv(1024)# recv是一个阻塞函数file_paramsjson.loads(data_json.decode())print(file_params:,file_params)file_sizefile_params[file_size]file_namefile_params[file_name]# 将接收到的文件数据一行行写入到新文件中receive_data_len0withopen(imgs/file_name,wb)asf:whilereceive_data_lenfile_size:tempconn.recv(1024)f.write(temp)receive_data_lenlen(temp)print(文件上传成功)