Contents
- 1. 实现如下效果
- 1.1. 要求
- 1.2. 分析
- 1.3. 代码实现
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行的内容,可以在代码中进行逻辑判断,即可解决。
至此,就实现了预期的要求,即在客户端中输入加减法表达式,并将这个表达式传递给服务器端,随后在服务器计算该表达式,并将结果返回给客户端,然后在客户端中进行打印输出。