Contents

  • 1. 实现如下效果
  • 1.1. 要求
  • 1.2. 分析
  • 1.3. 代码实现


1. 实现如下效果

1.1. 要求

  1. server端监听指定的tcp端口
  2. server端预先实现简单加减法的代码(可以自行扩展其他更复杂功能)
  3. client端可以通过socket连接与server端通信传输需要参数
  4. server端根据传的参数计算结果并返回
  5. 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行的内容,可以在代码中进行逻辑判断,即可解决。

至此,就实现了预期的要求,即在客户端中输入加减法表达式,并将这个表达式传递给服务器端,随后在服务器计算该表达式,并将结果返回给客户端,然后在客户端中进行打印输出。