赞
踩
网络编程是指在计算机网络上进行数据传输和通信的编程技术。它涉及到在不同计算机之间建立连接、发送和接收数据以及处理网络通信的各种操作。网络编程广泛应用于各种领域,包括服务器开发、Web开发、分布式系统、云计算等。
软件架构设计可以根据应用场景的不同分为客户端/服务器(Client/Server,CS)架构和浏览器/服务器(Browser/Server,BS)架构。
CS架构是一种常见的软件架构,它将软件系统划分为两个主要部分:客户端和服务器。客户端负责展示用户界面和处理用户输入,而服务器负责处理业务逻辑和存储数据。客户端和服务器之间通过网络进行通信,客户端发送请求给服务器,服务器进行处理并返回结果给客户端。
在CS架构中,客户端和服务器可以运行在不同的物理设备上,通过网络连接进行通信。客户端可以是桌面应用程序、移动应用程序等,而服务器可以是独立的物理服务器或云服务器。
例如:你电脑的上QQ、百度网盘、钉钉、QQ音乐 等安装在电脑上的软件。
服务端:互联网公司会开发一个程序放在他们的服务器上,用于给客户端提供数据支持。
客户端:大家在电脑安装的相关程序,内部会连接服务端进行收发数据并提供 交互和展示的功能。
BS架构是一种特殊的CS架构,其中客户端是通过浏览器访问应用程序,而服务器负责提供应用程序的逻辑和数据。在BS架构中,客户端使用浏览器作为用户界面,通过HTTP协议与服务器进行通信。
BS架构的优势在于客户端无需安装额外的软件,只需使用普通的浏览器即可访问应用程序。这使得应用程序的部署和维护更加方便,同时也提供了跨平台和跨设备的能力。
在BS架构中,服务器端主要负责业务逻辑和数据处理,而客户端主要负责展示和用户交互。服务器端可以使用不同的技术栈,如Web服务器、应用服务器、数据库服务器等。
例如:淘宝、京东等网站。
服务端:互联网公司开发一个网站,放在他们的服务器上。
客户端:不需要开发,用现成的浏览器即可。
需要注意的是,CS架构和BS架构并不是互斥的,而是根据应用场景和需求选择最适合的架构。某些应用可能需要同时使用CS和BS架构,例如在一个企业内部系统中,可以使用CS架构的桌面应用程序和BS架构的Web应用程序相结合,以满足不同的用户需求和使用场景。
TCP是面向连接的,可靠的。建立连接时要三次握手,取消链接是要四次挥手。UDP是不可靠的连接,一般应有在视频音频通过这种需要高传输效率的场景
TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的通信协议,数据在传输前要建立连接,传输完毕后还要断开连接。
客户端在收发数据前要使用 connect() 函数和服务器建立连接。建立连接的目的是保证IP地址、端口、物理链路等正确无误,为数据的传输开辟通道。
序号:Seq(Sequence Number)序号占32位,用来标识从计算机A发送到计算机B的数据包的序号,计算机发送数据时对此进行标记。
确认号:Ack(Number)确认号占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
状态,开始监听客户端请求。这个时候,客户端开始发起请求:
SYN-SEND
状态。SYN-RECV
状态。ESTABLISED
状态,表示连接已经成功建立。ESTABLISED
状态。至此,客户端和服务器都进入了ESTABLISED
状态,连接建立成功,接下来就可以收发数据了。注意:三次握手的关键是要确认对方收到了自己的数据包,这个目标就是通过“确认号(Ack)”字段实现的。计算机会记录下自己发送的数据包序号 Seq,待收到对方的数据包后,检测“确认号(Ack)”字段,看
Ack = Seq + 1
是否成立,如果成立说明对方正确收到了自己的数据包
TCP 是面向连接的传输协议,建立连接时要经过三次握手,断开连接时要经过四次握手,中间传输数据时也要回复 ACK 包确认,多种机制保证了数据能够正确到达,不会丢失或出错。
UDP 是非连接的传输协议,没有建立连接和断开连接的过程,它只是简单地把数据丢到网络中,也不需要 ACK 包确认。
UDP 传输数据就好像我们邮寄包裹,邮寄前需要填好寄件人和收件人地址,之后送到快递公司即可,但包裹是否正确送达、是否损坏我们无法得知,也无法保证。UDP 协议也是如此,它只管把数据包发送到网络,然后就不管了,如果数据丢失或损坏,发送端是无法知道的,当然也不会重发。
既然如此,TCP 应该是更加优质的传输协议吧?
如果只考虑可靠性,TCP 的确比 UDP 好。但 UDP 在结构上比 TCP 更加简洁,不会发送 ACK 的应答消息,也不会给数据包分配 Seq 序号,所以 UDP 的传输效率有时会比 TCP 高出很多,编程中实现 UDP 也比 TCP 简单。
UDP 的可靠性虽然比不上TCP,但也不会像想象中那么频繁地发生数据损毁,在更加重视传输效率而非可靠性的情况下,UDP 是一种很好的选择。比如视频通信或音频通信,就非常适合采用 UDP 协议;通信时数据必须高效传输才不会产生“卡顿”现象,用户体验才更加流畅,如果丢失几个数据包,视频画面可能会出现“雪花”,音频可能会夹带一些杂音,这些都是无妨的。
与 UDP 相比,TCP 的生命在于流控制,这保证了数据传输的正确性。
通过socket可以让我们不关心在传输数据的时候发生了什么,而专注于对数据进行传输
socket 的原意是“插座”,在计算机通信领域,它是计算机之间进行通信的一种约定或一种方式。通过 socket 这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据。 我们把插头插到插座上就能从电网获得电力供应,同样,为了与远程计算机进行数据传输,需要连接到因特网,而 socket 就是用来连接到因特网的工具。
socket是在应用层与传输层之间的一个抽象层,它的本质是编程接口,通过socket,才能实现TCP/IP协议。它就是一个底层套件,用来处理最底层消息的接受和发送。
根据数据的传输方式,可以将 Internet 套接字分成两种类型。通过 socket() 函数创建连接时,必须告诉它使用哪种数据传输方式。
流格式套接字(Stream Sockets)也叫“面向连接的套接字”,在代码中使用 SOCK_STREAM 表示。SOCK_STREAM 是一种可靠的、双向的通信数据流,数据可以准确无误地到达另一台计算机,如果损坏或丢失,可以重新发送。
SOCK_STREAM 有以下几个特征:
可以将 SOCK_STREAM 比喻成一条传送带,只要传送带本身没有问题(不会断网),就能保证数据不丢失;同时,较晚传送的数据不会先到达,较早传送的数据不会晚到达,这就保证了数据是按照顺序传递的。
为什么流格式套接字可以达到高质量的数据传输呢?这是因为它使用了 TCP 协议(The Transmission Control Protocol,传输控制协议),TCP 协议会控制你的数据按照顺序到达并且没有错误。
你也许见过 TCP,是因为你经常听说“TCP/IP”。TCP 用来确保数据的正确性,IP(Internet Protocol,网络协议)用来控制数据如何从源头到达目的地,也就是常说的“路由”。
那么,“数据的发送和接收不同步”该如何理解呢?
假设传送带传送的是水果,接收者需要凑齐 100 个后才能装袋,但是传送带可能把这 100 个水果分批传送,比如第一批传送 20 个,第二批传送 50 个,第三批传送 30 个。接收者不需要和传送带保持同步,只要根据自己的节奏来装袋即可,不用管传送带传送了几批,也不用每到一批就装袋一次,可以等到凑够了 100 个水果再装袋。
流格式套接字的内部有一个缓冲区(也就是字符数组),通过 socket 传输的数据将保存到这个缓冲区。接收端在收到数据后并不一定立即读取,只要数据不超过缓冲区的容量,接收端有可能在缓冲区被填满以后一次性地读取,也可能分成好几次读取。
也就是说,不管数据分几次传送过来,接收端只需要根据自己的要求读取,不用非得在数据到达时立即读取。传送端有自己的节奏,接收端也有自己的节奏,它们是不一致的。
数据报格式套接字(Datagram Sockets)也叫“无连接的套接字”,在代码中使用 SOCK_DGRAM 表示。
计算机只管传输数据,不作数据校验,如果数据在传输中损坏,或者没有到达另一台计算机,是没有办法补救的。也就是说,数据错了就错了,无法重传。
因为数据报套接字所做的校验工作少,所以在传输效率方面比流格式套接字要高。
可以将 SOCK_DGRAM 比喻成高速移动的摩托车快递,它有以下特征:
众所周知,速度是快递行业的生命。用摩托车发往同一地点的两件包裹无需保证顺序,只要以最快的速度交给客户就行。这种方式存在损坏或丢失的风险,而且包裹大小有一定限制。因此,想要传递大量包裹,就得分配发送。
另外,用两辆摩托车分别发送两件包裹,那么接收者也需要分两次接收,所以“数据的发送和接收是同步的”;换句话说,接收次数应该和发送次数相同。
总之,数据报套接字是一种不可靠的、不按顺序传递的、以追求速度为目的的套接字。
数据报套接字也使用 IP 协议作路由,但是它不使用 TCP 协议,而是使用 UDP 协议(User Datagram Protocol,用户数据报协议)。
QQ 视频聊天和语音聊天就使用 SOCK_DGRAM 来传输数据,因为首先要保证通信的效率,尽量减小延迟,而数据的正确性是次要的,即使丢失很小的一部分数据,视频和音频也可以正常解析,最多出现噪点或杂音,不会对通信质量有实质的影响。
注意:SOCK_DGRAM 没有想象中的糟糕,不会频繁的丢失数据,数据错误只是小概率事件。
socket翻译为套接字,可以把TCP/IP复杂的操作抽象为简单的几个接口来供应用层调用来实现进程在网络中的通信。socket起源于Unix,而Unix的基本要素之一就是“一切都为文件”,即可以通过打开——读写——关闭的模式来操作,通过这一点我们就可以来实现socket的简单编写
import socket from loguru import logger # (1) 构建服务端套接字对象 sock = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) # (2) 服务端三件套:bind listen accept sock.bind(("127.0.0.1", 8899)) sock.listen(5) logger.info("服务器启动") while 1: logger.info("等待新连接...") conn, addr = sock.accept() # 阻塞函数 # print(f"conn:{conn},addr:{addr}") logger.info(f"来自于客户端{addr}的请求成功") while 1: # (3) 收消息 data_bytes = conn.recv(1024) # 阻塞函数 print("data:", data_bytes.decode()) if data_bytes == "quit".encode() or len(data_bytes) == 0: logger.info(f"来自于{addr}客户端退出!") break # (4) 处理数据并发送 data = data_bytes.decode() res = data.upper() conn.send(res.encode())
import socket # (1) 构建客户端套接字对象 sock = socket.socket(family=socket.AF_INET, type=socket.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())
通过socket进行网络传输时如何传输大数据
因为默认最大一次可以传输1024字节。我们可以让客户端先给服务端传输一个数据长度,方便服务端计算出通过while循环接收几次
实现了一个基本的命令执行系统,允许客户端向服务器发送命令并获取执行结果。服务器接收到客户端的命令后,使用subprocess.getoutput()
执行命令,并将结果发送回客户端。客户端接收服务器返回的结果并进行处理。
import socket import subprocess import time import struct sock = socket.socket() sock.bind(("127.0.0.1", 6666)) sock.listen(5) while 1: conn, addr = sock.accept() print("客户端%s建立连接" % str(addr)) while 1: cmd = conn.recv(1024) # data字节串 if not cmd: print(f"{conn.getpeername()}客户端退出") break print("执行命令:", cmd.decode("gbk")) # 版本1 cmd_ret_bytes = subprocess.getoutput(cmd).encode() conn.send(cmd_ret_bytes) # 版本2 cmd_ret_bytes = subprocess.getoutput(cmd).encode() print("响应字节数", len(cmd_ret_bytes)) cmd_res_bytes_len = str(len(cmd_ret_bytes)).encode() conn.send(cmd_res_bytes_len) conn.send(cmd_res_bytes)
import socket import time ip_port = ("127.0.0.1", 6666) sk = socket.socket() sk.connect(ip_port) while 1: data = input("输入执行命令>>>") sk.send(data.encode()) # 版本1 res = sk.recv(1024) print("字节长度:", len(res)) print("执行命令结果:%s" % (res.decode())) # 版本2 cmd_ret_bytes_len = sk.recv(1024) cmd_res_len = int(cmd_ret_bytes_len.decode()) recv_num = 0 while recv_num < cmd_res_len: data = sk.recv(1024) print(data.decode()) recv_num += len(data) print("data的长度", recv_num, cmd_res_len)
在使用基于TCP的流格式套接字时,由于数据没有边界的特性,在网络延迟的时时候可能会出现粘包现象。(例如,如果发送端先传输数据长度,在传输数据,接收端可能会一起接收)要解决粘包现象,可以使用struct模块,利用struct.pack(‘i’,1345)对数据进行压缩,压缩成固定长的字节,让接收端第一次只接收固定长度的字节。
粘包(Packet Congestion)是计算机网络中的一个常见问题,粘包问题通常出现在使用面向连接的传输协议(如TCP)进行数据传输时,这是因为TCP是基于字节流的,它并不了解应用层数据包的具体边界。当发送端迅速发送多个数据包时,底层网络协议栈可能会将这些数据包合并成一个较大的数据块进行发送。同样地,在接收端,网络协议栈也可能将接收到的数据块合并成一个较大的数据块,然后交给应用层处理。
粘包问题可能导致数据处理的困难和不准确性。例如,在一个基于文本的协议中,接收方可能需要将接收到的数据进行分割,以便逐个处理每个完整的消息。如果数据包粘连在一起,接收方就需要额外的处理来确定消息的边界,这增加了复杂性。
demo演示
# 服务端 import socket import time s = socket.socket() s.bind(("127.0.0.1",8888)) s.listen(5) client,addr = s.accept() time.sleep(10) data = client.recv(1024) print(data) client.send(data) # 客户端 import socket s = socket.socket() s.connect(("127.0.0.1",8888)) data = input(">>>") s.send(data.encode()) s.send(data.encode()) s.send(data.encode()) res = s.recv(1024) print(res)
解决SSH案例的粘包
服务端
import socket import subprocess import time import struct sock = socket.socket() sock.bind(("127.0.0.1", 6666)) sock.listen(5) while 1: conn, addr = sock.accept() print("客户端%s建立连接" % str(addr)) while 1: cmd = conn.recv(1024) # data字节串 if not cmd: print(f"{conn.getpeername()}客户端退出") break print("执行命令:", cmd.decode("gbk")) # 版本3:粘包解决方案 result_str = subprocess.getoutput(cmd.decode("gbk")) result_bytes = bytes(result_str, encoding='utf8') res_len = struct.pack('i', len(result_bytes)) print("res_len:", res_len) conn.sendall(res_len) conn.sendall(result_bytes)
客户端
import socket import time import struct ip_port = ("127.0.0.1", 6666) sk = socket.socket() sk.connect(ip_port) while 1: data = input("输入执行命令>>>") sk.send(data.encode()) # 版本3:粘包解决方案 time.sleep(5) length_msg = sk.recv(4) length = struct.unpack('i', length_msg)[0] recv_num = 0 while recv_num < length: data = sk.recv(1024) print(data.decode()) recv_num += len(data) print("data的长度", recv_num, length)
客户端:
import socket import os import json # (1) 构建套接字对象,确定通信协议 sock = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) ip_port = ("127.0.0.1", 5566) sock.connect(ip_port) while 1: # 客户端给服务端发送消息 params = input("请输入命令(比如上传文件put 文件路径):") cmd, local_path = params.split(" ") # (1) 将文件信息传给服务端 file_size = os.path.getsize(local_path) file_name = os.path.basename(local_path) file_params = {"file_name": file_name, "file_size": file_size} sock.send(json.dumps(file_params).encode()) # (2) 循环读取文件,传给server端 with open(local_path, "rb") as f: for line in f: sock.send(line) print("文件发送完毕")
服务端:
import socket import json # (1) 构建套接字对象,确定通信协议 sock = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) # (2) 绑定IP和端口 ip_port = ("127.0.0.1", 5566) sock.bind(ip_port) # (3) 监听最大排队数 sock.listen(2) # (4) 阻塞等待客户端连接 while 1: print("server is waiting...") conn, addr = sock.accept() while 1: # 接受来自客户端的文件信息 data_json = conn.recv(1024) # recv是一个阻塞函数 file_params = json.loads(data_json.decode()) print("file_params:", file_params) file_size = file_params["file_size"] file_name = file_params["file_name"] # 将接收到的文件数据一行行写入到新文件中 receive_data_len = 0 with open("imgs/" + file_name, "wb") as f: while receive_data_len < file_size: temp = conn.recv(1024) f.write(temp) receive_data_len += len(temp) print("文件上传成功")
若有错误与不足请指出,关注DPT一起进步吧!!!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。