1. 实现如下效果
1.1. 要求
-
server端监听指定的tcp端口
-
server端预先实现简单加减法的代码(可以自行扩展其他更复杂功能)
-
client端可以通过socket连接与server端通信传输需要参数
-
server端根据传的参数计算结果并返回
-
client端打印返回结果
1.2. 分析
上述要求中,明确了需要使用tcp协议,所以必然用到socket套接字编程。另外需要实现服务器端:实现加减法代码,并将计算结果返回给客户端;客户端:给服务器端的加减法代码传递参数,并打印服务器端的计算结果。
-
服务器端分析
服务器端的代码实现,需要借助
socket
模块,由于这个模块中的一些方法式阻塞式的,在特定方法组赛的时候,为了使其他任务可以继续执行,此时还需要借助threading
模块实现多线程的操作。服务器端的
socket
编程,在主线程中创建了套接字对象之后,需要使用该套接字对象绑定到特定的IP地址以及端口(通过socket
模块创建的socket套接字对象提供的bind
方法完成绑定),并对该IP地址和端口进行监听(通过socket
模块创建的socket套接字对象提供的listen
方法对绑定的地址和端口进行监听)。随后就需要等待客户端发起连接请求,并且接受客户端的连接请求(通过socket
对象的accept
方法完成),随后就是通过socket对象的send
以及recv
方法实现服务器端与客户端之间的信息交互了。在下面的示例中,服务器端通过类实现,在类中除了实例对象初始化方法
__init__
之外,还提供了如下几个方法:def start(self):
在主线程之外,启动一个新的线程绑定到特定的IP地址以及端口上,并启动监听 。同时在这个新的线程中,再创建一个子线程,用于接受客户端的连接请求(因为socket对象的accept
方法为阻塞式方法,如果不在新的子线程中处理连接请求,会导致程序阻塞在accept
方法调用的地方)。def accept(self):
用于定义服务器端发现客户端连接请求的时候所执行的操作,如果客户端的连接请求正常建立,则保存该客户端的信息,同时创建一个新的线程,用于接收客户端发送的消息;否则抛出异常。def recv(self, c_sock: socket.socket, c_addr: tuple):
用于定义服务器端与客户端建立连接之后的操作,这个函数用于接收客户端发送给服务器端的信息,并将服务器端的处理结果发送给客户端。def stop(self):
用于定义服务器端结束的时候,所执行的操作。此时会将已经被记录的与客户端通信的套接字对象全部关闭,并且关闭主套接字对象。
在主线程中,定义了一个无限循环,在这个循环中,可以通过指令结束主线程的循环,否则主线程会持续保持运行状态。
-
客户端分析
客户端程序只需要使用socket套接字对象的
connect
方法,连接到服务器端的IP地址和端口号即可。随后就可以与服务器端进行消息交互了。在客户端的代码实现中,也是用类实现了客户端的主要功能,在其中除了定义了用于实例对象初始化的
__init__
方法之外,还定义了如下几个主要功能方法,具体如下所示:def start(self):
用于连接到IP地址和端口号指定的服务器端的套接字对象,并且在这个方法中,创建一个新的线程,用于接收来自服务器端的消息(由于socket对象的recv
方法是阻塞式方法,所以为了避免主线程被recv
方法阻塞,将recv方法调用放在一个新的线程中完成)。def recv(self):
用于接收来自服务器端的消息。def send(self, msg: str):
用于向服务器端发送消息。def stop(self):
用于停止客户端程序,关闭客户端的socket套接字对象。
在主线程中,创建了上述类的实例对象,并调用其
start
方法。同时在主线程中有一个无限循环,用于向服务器端发送消息,同时通过特定的输入内容,退出该无限循环。
1.3. 代码实现
-
服务器端的代码实现
服务器端的代码实现如下所示:
import logging import string import threading import socket """ 服务器端: 实现tcp网络通信,服务器端实现加减法,并将计算结果返回给客户端; 客户端给服务器端传递参数,并打印服务器端的计算结果。 """ FORMAT = "%(asctime)s %(threadName)s %(thread)d <<- %(message)s ->>" logging.basicConfig(format=FORMAT, level=logging.INFO) class SocketMathServer: def __init__(self, ip_addr, port_num): self.addr = ip_addr, port_num self._sock = socket.socket() self._event = threading.Event() self._lock = threading.Lock() self.clients = {} # self._data = None self._result = None def start(self): # 在主线程之外,启动一个线程接收客户端的连接请求 self._sock.bind(self.addr) self._sock.listen() thread_obj = threading.Thread(target=self.accept, name='accept thread') # accept方法为阻塞式方法 thread_obj.start() def accept(self): # recv方法也为阻塞式方法,在accept线程之外,启动一个线程接收客户端发送的消息 while not self._event.is_set(): try: client_sock, client_addr = self._sock.accept() except Exception as e: logging.info('quit server with {}'.format(e)) break else: with self._lock: self.clients[client_addr] = client_sock thread_obj = threading.Thread(target=self.recv, args=(client_sock, client_addr), name='recv thread') thread_obj.start() def recv(self, c_sock: socket.socket, c_addr: tuple): # 用于接收客户端发送的消息 while not self._event.is_set(): try: encode_data = c_sock.recv(1024) except Exception as e: logging.info('quit the server with {}'.format(e)) break else: data = encode_data.decode().strip() logging.info(data) target_str = string.digits + '+-' for s in ''.join(data.split(' ')): if s in target_str: continue else: err_msg = '<< {} >> is invalid math expression'.format(data) logging.info(err_msg) try: c_sock.send(err_msg.encode()) except Exception as e: logging.info(e) break else: self._result = eval(data) res_msg = 'the result of << {} is {} >>.'.format(data, self._result) c_sock.send(res_msg.encode()) if data == 'quit' or data == 'exit' or data == '': with self._lock: self.clients.pop(c_addr) c_sock.close() logging.info('{} quit'.format(c_addr)) break def stop(self): self._event.set() with self._lock: for sock in self.clients.values(): sock.close() self._sock.close() if __name__ == '__main__': addr = '127.0.0.1', 9988 s1 = SocketMathServer(*addr) s1.start() while True: cmd = input('if you want to exit server, please enter "quit" or "exit" >>> '.strip()) if cmd == 'quit' or cmd == 'exit': s1.stop() threading.Event().wait(3) break logging.info(threading.enumerate()) logging.info(s1.clients)
上述就是服务器端的代码实现。
-
客户端的代码实现
客户端的代码实现如下所示:
import logging import threading import socket """ 客户端: 实现tcp网络通信,服务器端实现加减法,并将计算结果返回给客户端; 客户端给服务器端传递参数,并打印服务器端的计算结果。 """ FORMAT = '%(asctime)s %(threadName)s %(thread)d %(message)s)' logging.basicConfig(format=FORMAT, level=logging.INFO) class SocketMathClient: def __init__(self, ip_addr, port_num): self.addr = ip_addr, port_num self._sock = socket.socket() self._event = threading.Event() def start(self): self._sock.connect(self.addr) # my_addr, my_port = self._sock.getsockname() # self._sock.send('{} is ready'.format((my_addr, my_port)).encode()) #msg = input() #encode_msg = msg #self.send(encode_msg) thread_obj = threading.Thread(target=self.recv, name='recv') thread_obj.start() def recv(self): while not self._event.is_set(): try: encode_data = self._sock.recv(1024) except Exception as e: logging.info('client receive error with << {} >>'.format(e)) break else: data = encode_data.decode().strip() logging.info('{}'.format(data)) def send(self, msg: str): encode_data = '{}\n'.format(msg.strip()).encode() self._sock.send(encode_data) def stop(self): client_ip, client_port = self._sock.getsockname() self.send('{} is quit'.format((client_ip, client_port))) self._sock.close() self._event.wait(3) self._event.set() logging.info('Client is over') if __name__ == '__main__': server_ip, server_port = '127.0.0.1', 9988 sc = SocketMathClient(server_ip, server_port) sc.start() while True: cmd = input("If you want to exit client, please enter 'quit' or 'exit' >>> ").strip() if cmd == 'quit' or cmd == 'exit': sc.stop() break sc.send(cmd)
上述就是客户端的代码实现。
-
执行结果
在PyCharm中,先运行服务器端程序,然后启动客户端程序,由于客户端程序中用于输入消息,并且接收服务器端的返回结果,所以主要观察客户端的交互输出接口。
-
客户端程序的输出结果
交互接口内容如下所示:
If you want to exit client, please enter 'quit' or 'exit' >>> 123 + 345 If you want to exit client, please enter 'quit' or 'exit' >>> 2022-01-28 15:35:31,940 recv 74320 the result of << 123 + 345 is 468 >>.) 456 + 789 2022-01-28 15:35:38,690 recv 74320 the result of << 456 + 789 is 1245 >>.) If you want to exit client, please enter 'quit' or 'exit' >>> 567 - 234 If you want to exit client, please enter 'quit' or 'exit' >>> 2022-01-28 15:35:48,135 recv 74320 the result of << 567 - 234 is 333 >>.) a12 + 3 2022-01-28 15:35:56,383 recv 74320 << a12 + 3 >> is invalid math expression) If you want to exit client, please enter 'quit' or 'exit' >>> 34-b If you want to exit client, please enter 'quit' or 'exit' >>> 2022-01-28 15:36:02,980 recv 74320 << 34-b >> is invalid math expression) quit 2022-01-28 15:36:05,549 recv 74320 client receive error with << [WinError 10053] An established connection was aborted by the software in your host machine >>) 2022-01-28 15:36:08,558 MainThread 74976 Client is over) Process finished with exit code 0
服务器端返回的结果记录在
<< >>
中间。当输入quit或者exit
的时候,就退出客户端的程序。 -
服务器端的输出结果
交互接口的内容如下所示:
if you want to exit server, please enter "quit" or "exit" >>>2022-01-28 15:35:31,940 recv thread 69676 <<- 123 + 345 ->> 2022-01-28 15:35:38,690 recv thread 69676 <<- 456 + 789 ->> 2022-01-28 15:35:48,135 recv thread 69676 <<- 567 - 234 ->> 2022-01-28 15:35:56,383 recv thread 69676 <<- a12 + 3 ->> 2022-01-28 15:35:56,383 recv thread 69676 <<- << a12 + 3 >> is invalid math expression ->> 2022-01-28 15:36:02,980 recv thread 69676 <<- 34-b ->> 2022-01-28 15:36:02,980 recv thread 69676 <<- << 34-b >> is invalid math expression ->> 2022-01-28 15:36:05,549 recv thread 69676 <<- ('127.0.0.1', 1739) is quit ->> 2022-01-28 15:36:05,549 recv thread 69676 <<- << ('127.0.0.1', 1739) is quit >> is invalid math expression ->> 2022-01-28 15:36:05,549 recv thread 69676 <<- [WinError 10054] An existing connection was forcibly closed by the remote host ->> 2022-01-28 15:36:05,549 recv thread 69676 <<- quit the server with [WinError 10054] An existing connection was forcibly closed by the remote host ->>
上述代码还有些小瑕疵,比如上述的第9行的内容,可以在代码中进行逻辑判断,即可解决。
至此,就实现了预期的要求,即在客户端中输入加减法表达式,并将这个表达式传递给服务器端,随后在服务器计算该表达式,并将结果返回给客户端,然后在客户端中进行打印输出。
-