赞
踩
目录
第二篇文章连接如下:计算机网络,python语言实现基于socket的网络编程(二)
计网实验课期末大作业,项目全流程记录。
刚刚在本地实现了文字、图片、文本文件、视频的传输,因此先记录一下项目的流程。后续会开发相关的网页并搭建数据库(便于实现身份验证)。
socket网络编程的原理这里简单提一下。我们知道在运输层只有TCP与UDP两种传输协议,前者是面向连接的,用于提供可靠的服务,而后者则是无连接的,提供不可靠但高效率的服务。
而我这次要做的是基于TCP传输协议的文件传输应用程序,所以下面的代码部分都是围绕TCP来的。应用程序由服务器和客户端两部分组成,这次先实现核心的文件传输功能,至于后续的多线程、身份验证和网页留到后面完善。
另外,这次只实现了客户端发送数据,服务器接收并保存在本地。
代码结构目前如下:
效果图:
命令行执行客户端发送数据
打印服务器运行过程,展示收到的文件名和部分数据内容
废话不多说,上代码。
代码主体就是一个接受数据的类,接下来对类中的方法进行介绍:
首先是构造函数部分
- def __init__(self):
- self.FORMAT = 'utf-8'
- self.HEADER = 64 # 与客户端约定好的,第一条信息(会告知数据的总长)的长度
- self.PORT = 5050
- self.SERVER = '192.168.246.53'
- self.ADDR = (self.SERVER, self.PORT)
定义了服务器的IP地址和端口,以及编码格式。其中的HEADER需要解释一下。在Python的socket模块中,recv()函数是无法提前知道要接受的数据的大小的,因此我看到很多的文章都是直接指定1024字节,这种方法固然很简便,但是适应性不强。无法满足不定长或者较大的文件传输需求。因此HEARDER在这里的作用就是:
通过为服务器和客户端共同指定一个发送数据前必须发送一个指明要发送数据的长度的额外数据(这里取名为header,意为首部数据、第一个数据)。这个数据以字符串的形式指明了下一个要发送的数据(也就是我们真正要传输的数据)的长度,这样下次再接收数据的时候就指定这个数据大小即可把发送方发送的全部数据完整收下,不多不少。可以节约很多的内存资源或者避免数据接收不完整的现象。
举个例子,我这里指定HEADER = 64,意味着每次服务器或客户端在接收或者发送数据前要做两件事情:(发送的情况)第一件事情,计算发送数据的长度,然后转换成字符型并进行编码得到二进制的形式。然后再对这个二进制数据用空格填充,填充满为64个字节。第二件事情,将这个二进制数据发送出去后,再发送要发送的真正的数据。
(接收的情况)第一件事情,利用recv()函数接受数据,解码后得到的是一个字符型的数字,用int()转成整数;第二件事情,再次接受数据,接受的字节就设定为前面的整数,这样就可以把数据完整的接收了。
还有一点要提一下。我这里每次发送一个文件实际上进行了三次数据的传输,除了上面说的两次传输外,最后我还传输了文件的名称。而传输的过程就是直接利用socket的send()函数,然后接收的那一方固定用HEADER大小接收就可以了,因为文件名不会太大,可以固定大小接收。
然后是启动服务器的代码
- def start(self):
- """启动服务器"""
- self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- self.server.bind(self.ADDR)
- self.server.listen()
- print(f"[LISTENING] Listinging on {self.SERVER}")
-
- while True:
- #conn是连接,addr就是客户端的IP地址和端口等信息
- conn, addr = self.server.accept()
- self.handle_img(conn, addr)
- break
- while True:
- self.handle_txt(conn, addr)
- break
- while True:
- self.handle_video(conn, addr)
- break
这里的步骤可以直接套用,是通用的。首先建立一个套接字对象,然后将之前的服务器的IP地址和端口号用元组的形式绑在一起,让套接字进行绑定。最后服务器开始监听来自客户端的连接请求(这里没有可以连接几个哦)
然后就是三个循环,在第一个循环中我们利用服务器接收到的连接,分别执行三个函数,用来接收三种类型的文件。三个函数后面会详细介绍。
处理文本文件的代码。
- def handle_txt(self, conn, addr, store_path='../store'):
- """
- 处理客户端传来的数据报(由首部和数据部分两部分组成)
- :param conn: 与客户端建立的连接
- :param adrr: 建立的连接的信息,包括客户端的ip地址和端口
- :param store_path: 写入本地文件夹的地址
- :return:
- """
- print(f"[TXT HANDLER] {addr[0]}:{addr[1]} has already connected to the server!")
-
- msg_length = conn.recv(self.HEADER).decode(self.FORMAT) # 接收数据的总长度,字符型
- # 有数据才执行下列操作
- if msg_length:
- msg_length = int(msg_length)
- msg = conn.recv(msg_length).decode(self.FORMAT) # 接收文件数据
- # 写入本地文件夹中
- filename = conn.recv(self.HEADER).decode(self.FORMAT) # 接收文件名
- with open(os.path.join(store_path, filename), 'w', encoding='utf-8') as f:
- f.write(msg)
-
- print(f"[FILE_NAME] {filename}")
- print(f"[RECEIVED DATA] {msg}")
这里我们将服务器启动后与客户端建立的连接和客户端的地址信息传入,同时指定了接收到的文件保存的位置。
然后就是第一次接收数据,得到的是等会要接受的数据的长度msg_length,然后判断是否收到数据,如果有数据的话则将长度转成整数型并传入第二个recv()函数中,得到真正的文本数据msg.
最后接收文件的名称filename,将文本数据写入到指定的保存地址store_path。
处理图片的代码
- def handle_img(self, conn, addr, store_path='../store'):
- """
- 处理客户端传来的数据报(由首部和数据部分两部分组成)
- :param conn: 与客户端建立的连接
- :param adrr: 建立的连接的信息,包括客户端的ip地址和端口
- :param store_path: 写入本地文件夹的地址
- :return:
- """
- print(f"[IMAGE HANDLER] {addr[0]}:{addr[1]} has already connected to the server!")
-
- msg_length = conn.recv(self.HEADER).decode(self.FORMAT) # 接收数据的总长度,字符型
- # 有数据才执行下列操作
- if msg_length:
- msg_length = int(msg_length)
- msg = conn.recv(msg_length) # 接收文件数据
- # 写入本地文件夹中
- filename = conn.recv(self.HEADER).decode(self.FORMAT) # 接收文件名
- with open(os.path.join(store_path, filename), 'wb') as f:
- f.write(msg)
-
- print(f"[IMAGE_NAME] {filename}")
处理图片的部分和文本文件的过程基本一致,唯一不同的部分,就是在写入的时候注意用"wb",因为是二进制的数据。
关于视频的传输也是一样的,这里就不再赘述了,有需要的同学可以看源码。
客户端对应的就是发送数据,同样是一个类解决问题。这里介绍类中的两个发送数据的方法。
发送文本的函数方法。
- def send_txt(self, txt_path):
- """
- 发送txt文本文件,步骤:
- 1. 发送数据总长
- 2. 发送文件数据
- 3. 发送文件名
- :param txt_path:文本文件的路径
- """
- txt_name = txt_path.split('/')[-1]
- with open(txt_path, 'r', encoding=self.FORMAT) as f:
- content = f.read().encode(self.FORMAT)
- msg_length = str(len(content)).encode(self.FORMAT) # 数据总长
- msg_length += b' ' * (self.HEADER - len(msg_length))
- self.client.send(msg_length)
- self.client.send(content)
- self.client.send(txt_name.encode(self.FORMAT))
首先获取文件的名称。然后打开文件读取其中的内容并进行编码
随后就是获取数据的长度msg_length并用字符串形式表示并编码成二进制。最后用空格进行填充,使大小为指定的HEADER = 64字节。
然后就发送表示数据长度的数据、数据本身、文件名
发送图片的方法
- def send_img(self, img_path):
- """
- 发送图片,步骤:
- 1. 发送数据总长
- 2. 发送图片数据
- 3. 发送图片名
- :param img_path: 图片所在路径
- """
- imgname = img_path.split('/')[-1]
- with open(img_path, 'rb') as f:
- content = f.read()
- msg_length = str(len(content)).encode(self.FORMAT) # 数据总长
- msg_length += b' ' * (self.HEADER - len(msg_length))
- self.client.send(msg_length)
- self.client.send(content)
- self.client.send(imgname.encode(self.FORMAT))
发送图片是一样的,需要注意的是图片用'rb'打开,无需指定编码格式因为本身就是二进制了。其他步骤同上。
所有的进展基本上就讲完了,还是很简单的。接下来要做的就是身份验证和多线程的问题了。因为代码还没写完,所以源代码就没有放出来,有需要的同学可以私信我。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。