网络编程
定义:所为网络编程即是对信息的发送和接收。
主要工作:
(1)发送端:将信息以规定的协议组装成数据包。
(2)接收端:对收到的数据包解析,以提取所需要的信息。
Socket:两个在网络上的程序通过一个双向的通信连接,实现数据的交换,此连接的一端称为一个socket。
Socket的本质:Socket是一个编程接口(API),TCP/IP协议需要向开发者提供做网络开发用的接口,这就是Socket接口,它是对TCP/IP协议网络通信的封装。
python中用有标准库socket,要进行socket编程,只需导入这个模块即可。
例一(实现一个单对单,只能发送一次消息的一次性服务端和客户端):
#服务端
import socket address = ("localhost", 6666) #写明服务端要监听的地址,和端口号
server = socket.socket() #生成一个socket对象
server.bind(address) #用socket对象绑定要监听的地址和端口
server.listen() #开始监听 conn,addr = server.accept() #等带新连接接入服务端,返回一个新的socket对象和地址,地址格式同前面格式
'''
Wait for an incoming connection. Return a new socket
representing the connection, and the address of the client.
'''
data = conn.recv(1024) #接收信息,写明要接收信息的最大容量,单位为字节
print("server recv:", data)
conn.send(data.upper()) #对收到的信息处理,返回到客户端 server.close() #关闭服务端
socket_server
#客户端
import socket address = ('localhost', 6666) #写明要发送消息的服务端的地址和端口号
client = socket.socket()
client.connect(address) #连接服务端 client.send(b"hell world") #发送信息,注意在python3中socket的发送只支持bytes类型
data = client.recv(1024) #等待接收服务端返回的信息
print("client recv:", data) client.close() #关闭客户端
socket_client
例二(对上面的代码进行改进,可以挂起多个连接,使每个连接可以进行多次对话且上一个连接断开后下一个连接马上接入):
#服务端
import socket address = ("localhost", 6666) #写明服务端要监听的地址,和端口号
server = socket.socket() #生成一个socket对象
server.bind(address) #用socket对象绑定要监听的地址和端口
server.listen(5) #开始监听 while True:
#一条连接关闭后,接入下一条连接
conn,addr = server.accept() #等带新连接接入服务端,返回一个新的socket对象和地址,地址格式同前面格式
'''
Wait for an incoming connection. Return a new socket
representing the connection, and the address of the client.
'''
while True:
#使其可以接收多次消息
data = conn.recv(1024) #接收信息,写明要接收信息的最大容量,单位为字节
# 没收到消息,断开本次连接
if not data:
break
print("server recv:", data)
conn.send(data.upper()) #对收到的信息处理,返回到客户端 server.close() #关闭服务端
socket_server2
#客户端
import socket address = ('localhost', 6666) #写明要发送消息的服务端的地址和端口号
client = socket.socket()
client.connect(address) #连接服务端 while True:
#使其可以向服务端多次发送消息
msg = input(">>>:").strip()
#如果发送的消息为空,则不再发送
if len(msg) == 0:
break
msg = msg.encode('utf-8') #将要发送的消息转为bytes类型
client.send(msg) #发送信息,注意在python3中socket的发送只支持bytes类型
data = client.recv(1024) #等待接收服务端返回的信息
print("client recv:", data.decode()) client.close() #关闭客户端
socket_client2
例三(对例二稍加改造,就可实现一个简单的ssh的服务端和客户端):
#服务端
import socket
import os address = ("localhost", 8888) #写明服务端要监听的地址,和端口号
server = socket.socket() #生成一个socket对象
server.bind(address) #用socket对象绑定要监听的地址和端口
server.listen() #开始监听 while True:
#一条连接关闭后,接入下一条连接
conn,addr = server.accept() #等带新连接接入服务端,返回一个新的socket对象和地址,地址格式同前面格式
'''
Wait for an incoming connection. Return a new socket
representing the connection, and the address of the client.
'''
while True:
data = conn.recv(1024) #接收信息,写明要接收信息的最大容量,单位为字节
# 没收到消息,断开本次连接
if not data:
break
cmd_result = os.popen(data.decode(), 'r').read() #执行命令,将命令执行结果保存到cmd_result
if len(cmd_result) == 0:
'''命令执行结果为空,认为接收到错误命令'''
cmd_result = "It's a wrong command..." while True:
conn.send(str(len(cmd_result)).encode('utf-8')) #发送命令执行结果的长度
confirm = conn.recv(1024).decode()
'''客户端确认收到数据长度,发送数据,否则重传;且解决粘包问题'''
if confirm == "OK":
conn.send(cmd_result.encode('utf-8')) #对收到的信息处理,返回到客户端
break
else :
continue server.close() #关闭服务端
ssh_socket_server
import socket address = ("localhost", 8888)
client = socket.socket()
client.connect(address) while True:
cmd = input("(command)>>>:").strip()
if len(cmd) == 0:
'''发送空命令时,结束本次循环'''
continue
if cmd == "#exit":
'''当检测到#exit,客户端与服务端断开连接'''
break client.send(cmd.encode()) #向服务端发送命令 cmd_result = '' #目前已接收的数据
size_data = 0 #目前已接收数据的长度
size_cmd_result = int(client.recv(1024).decode()) #接收命令执行结果的长度
client.send("OK".encode("utf-8")) #向服务端确认收到数据长度
while size_data < size_cmd_result:
'''命令的执行结果可能大于设置的接收buffersize,多次接收'''
data = client.recv(1024).decode() #每次接收的数据
size_data += len(data)
cmd_result += data print(cmd_result) client.close()
ssh_socket_client
注:提供另一种接收思路,服务端可以在每次返送完命令执行结果后,再发送一个结束标志,当客户端检测到结束标志时停止循环接收。
例四(改造例三,就可以实现一个简单的ftp的服务端和客户端)
#/usr/bin/python3
#服务端
import socket
import os
import hashlib address = ("0.0.0.0", 8888) #写明服务端要监听的地址,和端口号
server = socket.socket() #生成一个socket对象
server.bind(address) #用socket对象绑定要监听的地址和端口
server.listen() #开始监听 while True:
#一条连接关闭后,接入下一条连接
conn,addr = server.accept() #等带新连接接入服务端,返回一个新的socket对象和地址,地址格式同前面格式
'''
Wait for an incoming connection. Return a new socket
representing the connection, and the address of the client.
'''
while True:
content = os.popen('ls', 'r').read()
conn.send(content.encode('utf-8')) #与客户端建立连接后,将服务端有哪些文件发给客户端,供客户端选择
filename = conn.recv(1024).decode() #接收客户端发来的文件名 if os.path.isfile(filename):
'''文件存在,开始发送文件'''
file_md5 = hashlib.md5() #初始化MD5对象,用于传输完成后的校验
file_size = os.stat(filename)[6] #读取文件大小
conn.send(str(file_size).encode('utf-8')) #将文件size发给客户端
confirm = conn.recv(1024).decode() #等待客户端确认接收
if confirm == "OK":
'''发送文件数据'''
with open(filename, 'rb') as fp:
for line in fp:
file_md5.update(line)
conn.send(line) client_md5 = conn.recv(1024).decode() #传输完成后接收客户端发来的MD5
if file_md5.hexdigest() == client_md5:
'''确认文件传输未出错'''
conn.send("Success...".encode('utf-8'))
else :
'''文件传输出错,提示客户端删除重传'''
conn.send("This file is changed, please delete it and try again...".encode('utf-8')) #客户端未确认接收,重试
else :
conn.send("Error, try again...".encode('utf-8'))
continue else :
'''文件不存在,让客户端重新发送文件名'''
conn.send("The file name is wrong and try again".encode('utf-8')) server.close() #关闭服务端
ftp_socket_server
#/usr/bin/python3
import socket
import hashlib address = ("192.168.56.50", 8888)
client = socket.socket()
client.connect(address) while True:
content = client.recv(4096) #接收并打印服务端有哪些文件
print("Files List".center(75, '-'))
print(content.decode()) #向服务端发送想要接收的文件名
filename = input("(You want to get)>>>:").strip()
if filename == '#exit':
break
client.send(filename.encode('utf-8')) file_size = client.recv(1024).decode()
if file_size.isdigit():
'''文件大小是不小于0的数字,则文件存在,准备接收'''
file_size = int(file_size)
if file_size >= 0:
data_size = 0
data_md5 = hashlib.md5() #初始化MD5对象,用以向服务端校验
client.send("OK".encode('utf-8')) #向服务端确认接收数据
with open(filename, 'wb') as fp:
while data_size < file_size:
data = client.recv(1024)
data_md5.update(data)
data_size += len(data)
fp.write(data)
client.send(data_md5.hexdigest().encode('utf-8')) #发送服务端发送数据的MD5码
message = client.recv(1024).decode() #接收并打印服务端的校验信息
print(message)
print('\n\n\n')
else :
'''文件大小不是数字,则出错,打印服务端的提示信息'''
print(file_size)
continue client.close()
ftp_socket_client
注意:如果代码中有两个(或两个以上)socket.send()连在一起(中间无阻塞(比如time.slee(), socket.recv()等)),有粘包风险。
通过上面的代码编写,可以发现尽管socket已经简化了网络编程的过程,但还是给人一种面向过程的感觉。记下来的socketserver便解决了网络服务端编程过于繁琐的问题。
socketserver:
socketserver
模块简化了编写网络服务器的任务。
有四个基本的具体服务器类:
- class
socketserver.
TCPServer
(server_address, RequestHandlerClass, bind_and_activate=True) -
这使用Internet TCP协议,它在客户端和服务器之间提供连续的数据流。如果bind_and_activate为true,构造函数将自动尝试调用
server_bind()
和server_activate()
。其他参数传递到BaseServer
基类。
- class
socketserver.
UDPServer
(server_address, RequestHandlerClass, bind_and_activate=True) -
这使用数据报,其是可能在运输中不按顺序到达或丢失的信息的离散分组。参数与
TCPServer
相同。
- class
socketserver.
UnixStreamServer
(server_address, RequestHandlerClass, bind_and_activate=True) - class
socketserver.
UnixDatagramServer
(server_address, RequestHandlerClass, bind_and_activate=True) -
这些更常用的类与TCP和UDP类类似,但使用Unix域套接字;它们在非Unix平台上不可用。参数与
TCPServer
相同。
这四个类同时处理请求;每个请求必须在下一个请求开始之前完成。如果每个请求需要很长时间来完成,这是不合适的,因为它需要大量的计算,或者因为它返回了很多客户端处理速度慢的数据。解决方案是创建一个单独的进程或线程来处理每个请求; ForkingMixIn
和ThreadingMixIn
混合类可以用于支持异步行为。
创建服务器需要几个步骤。首先,您必须通过对BaseRequestHandler
类进行子类化并覆盖其handle()
方法来创建请求处理程序类;此方法将处理传入请求。其次,您必须实例化一个服务器类,将它传递给服务器的地址和请求处理程序类。然后调用服务器对象的handle_request()
或serve_forever()
方法来处理一个或多个请求。最后,调用server_close()
关闭套接字。
当从ThreadingMixIn
继承线程连接行为时,应该明确声明您希望线程在突然关闭时的行为。ThreadingMixIn
类定义了一个属性daemon_threads,它指示服务器是否应该等待线程终止。如果您希望线程自主行为,您应该明确地设置标志;默认值为False
,这意味着Python不会退出,直到ThreadingMixIn
创建的所有线程都退出。
#四个基本类的继承关系
+------------+
| BaseServer |
+------------+
|
v
+-----------+ +------------------+
| TCPServer |------->| UnixStreamServer |
+-----------+ +------------------+
|
v
+-----------+ +--------------------+
| UDPServer |------->| UnixDatagramServer |
+-----------+ +--------------------+
例五(我们使用socketserver实现例二):
import socketserver class MyTCPRequestHandler(socketserver.BaseRequestHandler):
"""
The request handler class for our server. It is instantiated once per connection to the server, and must
override the handle() method to implement communication to the
client.
"""
def handle(self):
"""
服务端和客户端连接后,数据的交互由这个方法实现,这个方法必须重写
"""
while True:
try:
self.data = self.request.recv(1024).strip()
print("server recv:", self.data.decode())
self.request.send(self.data.upper())
except ConnectionResetError:
break if __name__ == "__main__":
HOST, PORT = "localhost", 6666
server = socketserver.TCPServer((HOST, PORT), MyTCPRequestHandler)
server.serve_forever() #处理请求
server.server_close() #关闭套接字
TCPSever
注:服务器类具有相同的外部方法和属性,无论它们使用什么网络协议。
例六(使用ThreadingMixIn可以轻松实现一对多同时服务(多线程)
):
import socketserver class MyTCPRequestHandler(socketserver.BaseRequestHandler):
"""
The request handler class for our server. It is instantiated once per connection to the server, and must
override the handle() method to implement communication to the
client.
"""
def handle(self):
"""
服务端和客户端连接后,数据的交互由这个方法实现,这个方法必须重写
"""
while True:
try:
self.data = self.request.recv(1024).strip()
print("server recv:", self.data.decode())
self.request.send(self.data.upper())
except ConnectionResetError:
break if __name__ == "__main__":
HOST, PORT = "localhost", 6666
server = socketserver.ThreadingTCPServer((HOST, PORT), MyTCPRequestHandler) #同时处理多个连接
server.serve_forever() #处理请求
server.server_close() #关闭套接字 #ThreadingTCPServer的源码
'''
class ThreadingTCPServer(ThreadingMixIn, TCPServer): pass
'''
ThreadingTCPServer
注:其他协议的多线程实现同上。
PS(本例中所涉及到的模块使用参考):
socket模块:http://www.cnblogs.com/God-Li/p/7625825.html
hashlib模块:http://www.cnblogs.com/God-Li/p/7631604.html
os模块:http://www.cnblogs.com/God-Li/p/7384227.html
socketserver模块:http://python.usyiyi.cn/translate/python_352/library/socketserver.html#module-socketserver