import socket
import json
import random
import string
import time
# 服务器配置
HOST = '' # 绑定所有网卡(支持远程连接),本地测试可改为 '127.0.0.1'
PORT = 9999 # 自定义端口(避免与其他程序冲突)
BUFFER_SIZE = 1024
def generate_stimulus_sequence(n, length):
"""
生成 N-Back 刺激序列和匹配标记列表
:param n: N值(当前刺激与n步前比较)
:param length: 刺激序列长度
:return: stimulus_list(刺激序列),match_list(匹配标记,True=匹配)
"""
stimulus_list = [random.choice(string.ascii_uppercase) for _ in range(length)]
match_list = [False] * length
# 从第n个索引开始,判断是否与n步前匹配
for i in range(n, length):
if stimulus_list[i] == stimulus_list[i - n]:
match_list[i] = True
return stimulus_list, match_list
def main():
# 创建 TCP Socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 允许端口复用,避免程序重启后端口占用问题
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 绑定地址和端口
server_socket.bind((HOST, PORT))
# 监听客户端连接(最大等待数5)
server_socket.listen(5)
print(f"服务器已启动,监听端口 {PORT},等待客户端连接...")
# 等待客户端连接
client_socket, client_addr = server_socket.accept()
print(f"客户端已连接:{client_addr[0]}:{client_addr[1]}")
try:
# 1. 定义任务参数(可自定义修改)
task_params = {
"n": 2, # N-Back的N值(2-back任务)
"stimulus_length": 20, # 刺激序列总长度
"display_time": 2, # 每个刺激呈现时间(秒)
"prompt_time": 1 # 刺激间隔提示时间(秒)
}
# 发送任务参数给客户端(JSON序列化 + 编码)
client_socket.send(json.dumps(task_params).encode('utf-8'))
print("已发送任务参数给客户端")
# 2. 生成刺激序列和匹配列表
n = task_params["n"]
stimulus_length = task_params["stimulus_length"]
stimulus_list, match_list = generate_stimulus_sequence(n, stimulus_length)
print("已生成刺激序列,开始执行N-Back任务...")
# 3. 逐次发送刺激并接收客户端反馈
result_list = [] # 存储客户端反馈结果(is_correct, reaction_time)
for idx, stimulus in enumerate(stimulus_list):
# 发送当前刺激给客户端
client_socket.send(stimulus.encode('utf-8'))
print(f"发送刺激 {idx+1}/{stimulus_length}:{stimulus}")
# 接收客户端反馈(JSON格式)
feedback_data = client_socket.recv(BUFFER_SIZE).decode('utf-8')
feedback = json.loads(feedback_data)
is_correct = (feedback["user_choice"] == 1 and match_list[idx]) or (feedback["user_choice"] == 0 and not match_list[idx])
reaction_time = feedback["reaction_time"]
result_list.append((is_correct, reaction_time))
# 等待刺激间隔时间(模拟任务节奏)
time.sleep(task_params["prompt_time"])
# 4. 统计任务结果
total_trials = len(result_list)
correct_trials = sum([1 for is_correct, _ in result_list if is_correct])
accuracy = (correct_trials / total_trials) * 100
valid_reaction_times = [rt for _, rt in result_list if rt > 0]
avg_reaction_time = sum(valid_reaction_times) / len(valid_reaction_times) if valid_reaction_times else 0
# 打印服务器端统计结果
print("\n===== 任务结束 - 服务器统计结果 =====")
print(f"N值:{n}")
print(f"总试次:{total_trials}")
print(f"正确试次:{correct_trials}")
print(f"正确率:{accuracy:.2f}%")
print(f"平均反应时:{avg_reaction_time:.2f} 秒")
# 5. 发送结束信号和统计结果给客户端
end_data = {
"task_end": True,
"accuracy": accuracy,
"avg_reaction_time": avg_reaction_time
}
client_socket.send(json.dumps(end_data).encode('utf-8'))
except Exception as e:
print(f"任务异常:{e}")
finally:
# 关闭连接
client_socket.close()
server_socket.close()
print("服务器连接已关闭")
if __name__ == "__main__":
main()
import socket
import json
import tkinter as tk
from tkinter import messagebox
import time
import threading
# 客户端配置
SERVER_PORT = 9999
BUFFER_SIZE = 1024
# 全局变量(存储任务状态和反应数据)
current_stimulus = ""
stimulus_show_time = 0.0
user_choice = -1 # -1=未输入,0=不匹配,1=匹配
input_lock = threading.Lock()
def create_gui():
"""创建tkinter图形界面"""
root = tk.Tk()
root.title("N-Back 客户端")
root.geometry("400x300") # 窗口大小
# 刺激显示标签(大号字体)
stimulus_label = tk.Label(root, text="等待任务开始...", font=("Arial", 48, "bold"))
stimulus_label.pack(expand=True, pady=50)
# 状态提示标签
status_label = tk.Label(root, text="状态:未连接", font=("Arial", 12))
status_label.pack(pady=10)
# 绑定键盘事件(1=匹配,0=不匹配)
def on_key_press(event):
global user_choice, stimulus_show_time
with input_lock:
if user_choice == -1 and event.char in ["0", "1"]:
# 计算反应时(从刺激呈现到按键的时间差)
reaction_time = time.time() - stimulus_show_time
user_choice = int(event.char)
status_label.config(text=f"已输入:{user_choice} | 反应时:{reaction_time:.2f} 秒")
root.bind("<Key>", on_key_press)
return root, stimulus_label, status_label
def main():
global current_stimulus, stimulus_show_time, user_choice
# 1. 获取服务器IP(用户输入)
server_ip = input("请输入服务器IP地址(本地测试输入127.0.0.1):").strip()
if not server_ip:
server_ip = "127.0.0.1"
# 2. 创建GUI界面
root, stimulus_label, status_label = create_gui()
status_label.config(text=f"正在连接服务器 {server_ip}:{SERVER_PORT}...")
root.update() # 更新界面
try:
# 3. 创建TCP Socket并连接服务器
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect((server_ip, SERVER_PORT))
status_label.config(text=f"已连接服务器:{server_ip}:{SERVER_PORT}")
root.update()
# 4. 接收任务参数
task_params_data = client_socket.recv(BUFFER_SIZE).decode('utf-8')
task_params = json.loads(task_params_data)
n = task_params["n"]
stimulus_length = task_params["stimulus_length"]
display_time = task_params["display_time"]
prompt_time = task_params["prompt_time"]
print(f"接收任务参数:N={n},刺激长度={stimulus_length},呈现时间={display_time}秒")
status_label.config(text=f"任务参数:{n}-Back | 总试次:{stimulus_length}")
root.update()
time.sleep(2) # 提示延迟
# 5. 逐次接收刺激并反馈
for idx in range(stimulus_length):
# 重置用户输入状态
user_choice = -1
# 接收当前刺激
stimulus = client_socket.recv(BUFFER_SIZE).decode('utf-8')
current_stimulus = stimulus
# 呈现刺激并记录呈现时间
stimulus_label.config(text=stimulus)
status_label.config(text=f"试次 {idx+1}/{stimulus_length} | 按1=匹配,0=不匹配")
root.update()
stimulus_show_time = time.time()
# 等待用户输入或刺激呈现超时
start_wait = time.time()
while user_choice == -1:
if time.time() - start_wait > display_time:
status_label.config(text="超时未输入!")
root.update()
break
root.update_idletasks()
root.after(10) # 避免界面卡死
# 构造反馈数据
reaction_time = time.time() - stimulus_show_time if user_choice != -1 else 0
feedback = {
"user_choice": user_choice,
"reaction_time": reaction_time
}
# 发送反馈给服务器
client_socket.send(json.dumps(feedback).encode('utf-8'))
# 刺激间隔时间
stimulus_label.config(text="")
root.update()
time.sleep(prompt_time)
# 6. 接收服务器统计结果并显示
end_data = client_socket.recv(BUFFER_SIZE).decode('utf-8')
end_result = json.loads(end_data)
if end_result["task_end"]:
accuracy = end_result["accuracy"]
avg_reaction_time = end_result["avg_reaction_time"]
# 显示任务结果
result_text = f"===== 任务结束 =====\nN值:{n}\n总试次:{stimulus_length}\n正确率:{accuracy:.2f}%\n平均反应时:{avg_reaction_time:.2f} 秒"
stimulus_label.config(text="任务结束")
status_label.config(text=result_text)
messagebox.showinfo("任务结果", result_text)
root.update()
except ConnectionRefusedError:
messagebox.showerror("错误", "无法连接服务器,请确认服务器已启动且IP正确")
status_label.config(text="连接失败!")
except Exception as e:
messagebox.showerror("错误", f"客户端异常:{e}")
finally:
# 关闭连接和GUI
try:
client_socket.close()
except:
pass
root.mainloop() # 保持GUI窗口显示
if __name__ == "__main__":
main()