写一个适合自己游戏的简单的协议测试(接口测试)工具/抓包工具
cac55 2024-10-02 11:28 27 浏览 0 评论
曾几何时,wpe 是一个网络游戏抓包神器,我们在做协议测试的时候,也是通过 wpe 来抓包、改包和发包,来测试服务器对于非法协议的处理逻辑是否正确。wpe 操作简单,只需要加载进程,启动抓包,停止抓包就能看到所有与服务器之间的封包记录,但美中不足的是,wpe 抓取的封包内容全部是 16 进制显示,有时候我们需要改动某个字段,还需要先破译并找到字段位置来做修改,可读性比较差。
随着时代的发展,现在的游戏做了很多的安全措施,wpe 已无法找到目标游戏的进程,其他的抓包工具也很少有能满足测试需求的。而且,我们公司的测试组有自己编写的机器人,在之前,客户端和服务端之间详细的交互逻辑,我们知道的并不是很清楚,比如我执行一个操作之后,客户端会发送哪些协议,会收到哪些协议返回?协议的先后顺序是怎样的?在不知道这些信息的时候,机器人的脚本行为编写很慢,效率也比较低。
为了更好地配合机器人脚本的编写,以及针对我们的游戏项目进行协议测试,我们写了一个简单的小工具来达到显示封包收发过程以及插包改包来验证服务器逻辑的目的(PS:仅适合针对自己公司内部项目,因为需要知道协议文档嘿嘿),通过这个工具,客户端与服务端的每一条协议交互,都会按照先后顺序进行明文显示,这样在我们编写机器人行为的时候,也能够帮我们更好的梳理机器人协议发送的流程,更快的完成行为方法的编写,另外因为进行了明文显示,在协助我们做协议测试方面也比 wpe 更好。接下来我们来实现这个抓包发包工具。
首先我们分析一下 wpe 的原理(个人猜测,如有问题欢迎指正):
图有点乱,不过它大概是下面这样的:
1.wpe 加载客户端进程,然后将修改后的 dll 注入到客户端进程,hook 客户端的 send 和 recv 方法调用。
2.客户端进行发包操作,协议在通过客户端的 socket 时,会通过注入的 dll 进行处理,然后再发给服务端(处理可能包括拦截、修改等)。
3.服务端在返回封包之后,也会通过客户端的 socket,调用 dll 进行处理,然后再发给客户端。
4.wpe 可以直接插入伪装包,通过 dll 调用直接将伪装的封包通过客户端的 socket 发给服务端。
这里面最关键的是 dll 注入和 hook 钩子,不过这两个我研究了一阵子,没研究懂,所以我不会,尴尬...不过没关系,接下来我们换一种思路来做。
首先来分析一下我们想要的功能,请看下图(图片仅供参考,但需要自行使用 QT 设计一个类似的界面出来):
这个是目前已经做出来的一个小工具,大概分了几个区块:
1.目标服务器:就是游戏需要登录的那个服务器地址
2.进制转换功能:做这个东西主要是为了方便解读十六进制,当时用 wpe 测试的时候,解析字节流太繁琐了,产生了心理阴影。
3.协议生成和发送:可以填入协议号和参数组自动生成十六进制的协议,也可以模拟发送,实现 wpe 的插包功能。
4.其他功能:其他想要实现的辅助测试的功能,需要自己写。
5.协议记录:发送协议和接收协议的全部记录。
6.日志:记录一些过程和错误内容。
7.代理:工具的核心,实现所有的协议转发和修改功能。
刚刚讲到了,dll 注入有点难,我们需要换一种思路,上面这个小工具我是用代理 agent 的方式来实现的,原理图如下:
从图中可以看到,原本客户端与服务器是通过 socket 直连的,但是我们无法注入 dll 的话,就无法对收发过程进行干预,所以我们就需要在他们之间加一个代理,客户端发给服务器的包,先发到代理这里,然后代理再转发给服务器,同样的,服务器发给客户端的包,也是先发到代理这里,然后再转发给客户端。由于代理的功能是我们自己来实现的,所以在接收到客户端发给服务器的或者服务器返回的封包时,我们可以对其进行任意的修改。
接下来我们需要设计工具界面,并实现这个工具的各个功能:
一、代理
由于各个模块都与代理的功能挂钩,所以我们先实现代理的功能,以便后续关联 UI 的时候方便调试。
先来看下代理原理分析图:
1. 代理 server 启动监听,当客户端尝试连接服务器的时候,创建一个代理与客户端之间的链接 C_A_socket。
2. 然后创建一个代理与服务器之间的链接 S_A_socket。
3. 调用代理的 start 方法,启动发包、收包、解析等线程。发包线程持续从 C_A_socket 处 recv,收到协议之后根据需求进行一定的处理 (修改、拦截等),然后从 S_A_socket 处 send 给服务器,收包线程刚好相反,从 S_A_socket 处 recv,然后从 C_A_socket 处 send 给客户端,完成协议的转发。解析线程是将收发协议进行明文化解析,显示到工具上,方便查看。
接下来我们来实现代理的功能:
首先我们写一个 AgentServer 类,这个类启动后,绑定一个本地端口并进行监听,当监听到客户端连接后,启动一个代理实例。
class AgentServer:
def __init__(self, agent_port, server_host, server_port, ui):
"""
agent_server初始化
:param agent_port: 本地代理的绑定端口
:param server_host: 服务器的ip
:param server_port: 服务器的端口
"""
self.ui = ui # 工具ui,后面跟工具做关联的时候会用到 # 初始化一个socket self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 将socket绑定本地ip和端口进行监听 self.socket.bind(('127.0.0.1', agent_port))
self.socket.listen(5)
self.server_host = server_host
self.server_port = server_port
# 生成一个空的代理对象 self.agent_obj = None
def start(self):
"""
启动agent_server,等待客户端的链接请求
:return:
"""
while True:
# 等待客户端连接后获取到与客户端的socket client_socket, addr = self.socket.accept()
# 创建一个与服务器连接的socket server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.connect((self.server_host, self.server_port))
# 生成一个代理实例,将client_socket和server_socket传给代理 self.agent_obj = socket_agent(client_socket, server_socket, self.ui)
# 启动代理 self.agent_obj.start()
def stop(self):
"""
停止agent_server,关闭socket监听,并停止agent
:return:
"""
# 关闭agent_server的socket self.socket.close()
# 停止agent_obj self.agent_obj.stop()
接下来我们写一个代理类,Agent,并实现它的 start 和 stop 方法。
class Agent:
def __init__(self, client_socket, server_socket, ui):
"""
初始化一个agent代理
:param client_socket: 与客户端之间的socket连接
:param server_socket: 与服务器之间的socket连接
"""
self.ui = ui # 工具ui,后面跟工具做关联的时候会用到 self.client_socket = client_socket
self.server_socket = server_socket
self.sock_Num = 0 # 验证码,游戏发包验证 self.send_data = b'' # 发送协议包池 self.recv_data = b'' # 接收协议包池 self.alive = True # 是否存活 self.hide_proto = False # 是否屏蔽协议 self.loop_send = False # 循环发送状态
start 方法后面再写,先写一个 stop 方法
def stop(self):
"""
停止转发器,关闭所有线程
:return:
"""
self.alive = False
try:
self.client_socket.shutdown(2)
self.client_socket.close()
except Exception as e:
print('socket 关闭时出现了异常', e)
try:
self.server_socket.shutdown(2)
self.server_socket.close()
except Exception as e:
print('socket 关闭时出现了异常', e)
写一个客户端协议处理方法,从客户端处持续 recv 字节流,并放入包池中。这里不直接把字节流转发给服务器,因为我们可能需要对它做一些处理,如修改发包顺序验证码等。
def agent_clinet_to_server(self):
"""
处理客户端发送给服务器的包
1.获取客户端发起的字节流
2.将字节流全部粘贴在一起进行后续处理
3.此处不调用server socket进行转发,因为发送协议需要进行一些特殊的处理
4.循环此步骤
:return:
"""
# 首先判断agent是否存活状态 if self.alive:
try:
# 从客户端处接收字节流 data = self.client_socket.recv(65535)
if data:
# 将收到的字节流放入包池中等待后续处理 self.send_data += data
# 重新调用这个方法接收客户端的封包 Timer(0, self.agent_clinet_to_server).start()
else:
print('收到了空字节流,socket连接可能已经断开', data)
except OSError as e:
# 如果触发了OSError异常,说明socket可能已经断开了 self.alive = False
print('连接已断开!', e)
except:
Timer(0, self.agent_clinet_to_server).start()
写一个服务器协议转发方法,从服务器持续 recv 字节流,并放入包池中待解析,然后收到的字节流直接转发给客户端,因为服务器返回的协议我们无需特殊处理。
def agent_server_to_client(self):
"""
处理服务器发送给客户端的包
1.获取服务器返回的字节流
2.将字节流全部粘贴在一起进行后续解析显示
3.调用client socket,将服务器返回的字节流直接转发给客户端
4.循环此步骤
:return:
"""
if self.alive:
try:
# 从服务器处接收字节流 data = self.server_socket.recv(65535)
if data:
# 通过client_socket直接转发给客户端 self.client_socket.send(data)
# 同时将受到的字节流放入包池中等待解析 self.recv_data += data
# 重新调用这个方法接收服务器的封包 Timer(0, self.agent_server_to_client).start()
except OSError as e:
# 如果触发了OSError异常,说明socket可能已经断开了 self.alive = False
print('连接已断开!', e)
except:
Timer(0, self.agent_server_to_client).start()
接下来写一个修改发送包,转发给服务器并显示到 UI 上的方法,在这个方法里面调用了两个外部方法,分别是 edit_bytes 和 make_send_proto_msg。
edit_bytes 方法是将原本的客户端协议替换发包顺序验证码(比如验证码的顺序在客户端那里是自加 1,如果我们插个包发给服务器,要修改这个验证码 +1,但是客户端自身的验证码还是原来那个,所以会导致后面的协议验证码冲突,简单粗暴的做法就是所有的验证码都从代理工具生成,客户端发过来的协议验证码被无条件替换)。
make_send_proto_msg 方法是将协议 obj 转化成一个解析后的字符串 (具体可以看上面那个工具图的协议接收和发送记录那里,我是直接调用的协议文档转成的 python 文件,由于涉及到文档内容,这里没有列出,请见谅,这里只要实现一个方法,将协议封包对象明文显示即可),解析错误的话就将错误信息发送到日志文本展示框那里进行展示,这两个方法的具体实现逻辑这里就不写了。
def get_and_analyze_one_send_proto(self):
"""
拆解一个发送包并进行明文化显示到工具UI上
然后将这个包做一定修改后发给服务器
:return:
"""
if self.alive:
# 做一下封包完整性判断(7是根据协议结构来的) while len(self.send_data) >= 7 and self.alive:
# 获取协议的协议长度 length = struct.unpack("!H", self.send_data[0:2])[0]
# 判断是否能拆解出一个完整的封包 if length <= len(self.send_data) - 7:
# 通过edit_bytes编辑验证码并重新序列化协议 # PS:由于插包会改变原本的协议顺序验证码 # 所以我们让所有的封包都按照我们的验证码规则来 send_data, proto, self.sock_Num = edit_bytes(
self.send_data[0:7 + length], self.sock_Num)
# 将修改后的协议通过server_socket发送给服务器 self.server_socket.send(send_data)
# 判断是否屏蔽协议 if not self.hide_proto:
# 将通过make_send_proto_msg方法解析的协议内容显示到ui上 self.ui.textBrowser_send.append(
make_send_proto_msg(proto, send_data))
# 截取包池,抛弃已经处理的字节流 self.send_data = self.send_data[7 + length:]
# 重新调用此方法 Timer(0, self.get_and_analyze_one_send_proto).start()
写一个解析接收包,并显示到 UI 上的方法,make_recv_proto_msg 方法同样是将协议 obj 转化成一个解析后的字符串,解析错误的话就将错误信息发送到日志文本展示框那里进行展示,同样的,这个方法的具体实现逻辑这里不再说明。
PS:这两个方法中的 6 和 7 是根据项目的协议格式来的,我们项目的格式是 16 位的协议长度,32 位的协议 id,8 位的验证码,因此是 2+4+1=7,由于服务器返回的协议不需要验证码,所以是 6
def get_and_analyze_one_recv_proto(self):
"""
定义一个拆包并解析这个包的方法
服务器返回的包我们在recv的时候已经直接转发了,所以这里没再转发
:return:
"""
if self.alive:
# 做一下封包完整性判断(6是根据协议结构来的,比上面少了1位的验证码) while len(self.recv_data) >= 6 and self.alive:
# 解析出协议的长度和协议号 length, proto = struct.unpack("!HI", self.recv_data[0:6])
# 判断是否能拆解出一个完整的封包 if length <= len(self.recv_data) - 6:
# 判断是否屏蔽协议 if not self.hide_proto:
# 将通过make_recv_proto_msg方法解析的协议内容显示到ui上 self.ui.textBrowser_recv.append(make_recv_proto_msg(proto, self.recv_data[0:6 + length]))
# 截取包池,抛弃已经处理的字节流 self.recv_data = self.recv_data[6+length:]
# 重新调用此方法 Timer(0, self.get_and_analyze_one_recv_proto).start()
写一个发送伪造包的方法。
def agent_insert_send(self, data, times=1, sleep_time=100):
"""
插入一个伪造的数据包,并发送
:param data:伪造的数据包
:param times:发送次数
:param sleep_time:间隔时间
:return:
"""
try:
if self.alive:
if times:
for i in range(times):
data, proto, self.sock_Num = edit_bytes(data, self.sock_Num)
self.server_socket.send(data)
time.sleep(sleep_time/1000.0)
else:
while self.loop_send:
data, proto, self.sock_Num = edit_bytes(data, self.sock_Num)
self.server_socket.send(data)
time.sleep(sleep_time / 1000.0)
except Exception as e:
print("请先开启客户端并连接服务器!", e)
最后我们再回头写 start 方法。
def start(self):
"""
启动转发线程
启动拆包解包线程
:return:
"""
Timer(0, self.agent_clinet_to_server).start()
Timer(0, self.agent_server_to_client).start()
Timer(0, self.get_and_analyze_one_send_proto).start()
Timer(0, self.get_and_analyze_one_recv_proto).start()
至此,agent_server 和 agent 就写完了,接下来我们让他俩跟 UI 进行绑定。
二、模块绑定
1.服务器相关区块:
PS:以下方法是写在界面类下面的。
- 界面初始化的时候进行按钮点击事件绑定:
# 绑定启动服务按钮事件
self.ui.start_btn.clicked.connect(self.wpe_start)
# 绑定停止抓包按钮事件
self.ui.stop_btn.clicked.connect(self.wpe_stop)
- 实现这两个点击事件的方法,和配套的 QT 线程:
def wpe_start(self):
"""
启动中转代理agent_server,等待客户端连接
:return:
"""
try:
from pyqt_thread import begin_agent_service
self.agent_thread = begin_agent_service(From=self)
self.agent_thread.start()
except Exception as e:
print('启动失败!出现了异常', e)def wpe_stop(self):
"""
停止中转socket,关闭全部socket连接
:return:
"""
self.ui.textBrowser_send.append("*********服务已停止********")
self.ui.textBrowser_recv.append("*********服务已停止********")
self.agent_server.stop()class begin_agent_service(QThread):
"""
启动转发代理的thread类
"""
def __init__(self, parent=None, From=None):
super(begin_agent_service, self).__init__(parent)
self.ui = From
def run(self):
# 获取server_ip, server_port, my_port server_ip = self.ui.ui.ip_edit_line.text()
server_port = self.ui.ui.port_edit_line.text()
my_port = self.ui.ui.host_port_edit_line.text()
if server_ip and self.ui.ui.host_port_edit_line.text() and self.ui.ui.port_edit_line.text():
if self.ui.get_server_ip():
# 提示开始 self.ui.ui.textBrowser_send.append(
"*********代理已启动,请开启游戏**********")
self.ui.ui.textBrowser_recv.append(
"*********代理已启动,请开启游戏**********")
# AgentServer实例 self.ui.agent_server = AgentServer(
int(my_port), server_ip, int(server_port), self.ui)
Timer(0, self.ui.agent_server.start).start()
print('服务已启动')
else:
# 加个ip格式错误提示 for i in range(10):
self.ui.ui.ip_edit_line.setText('ip格式错误!')
time.sleep(0.05)
self.ui.ui.ip_edit_line.clear()
time.sleep(0.05)
self.ui.ui.ip_edit_line.setText(server_ip)
else:
# 加个参数错误提示 for i in range(10):
self.ui.ui.ip_edit_line.setText('三参不能为空!')
time.sleep(0.05)
self.ui.ui.ip_edit_line.clear()
time.sleep(0.05)
self.ui.ui.ip_edit_line.setText(server_ip)
2.进制转化区块
输入格式参考的是 wpe 那种显示格式,因为当时做的时候主要是针对 wpe 数据进行解析用的。
- 按钮点击事件绑定
# 绑定转换int按钮事件
self.ui.h2i_btn.clicked.connect(self.on_h2i_btn_click)
# 绑定转换str按钮事件
self.ui.h2s_btn.clicked.connect(self.on_h2s_btn_click)
- 实现这两个事件的方法
def on_h2i_btn_click(self):"""
点击转换为int按钮之后的处理方法
读取文本框中的16进制内容,转换为int类型
:return:
"""# 获取文本框中的内容try:
msg = self.ui.hex_input.toPlainText()
result = str(int(msg.replace(" ", "").lower(), 16))
self.ui.hex_tran_result.setText(result)except:
self.ui.hex_tran_result.setText('输入内容有误,请检查!!!')def on_h2s_btn_click(self):"""
点击转换为str按钮之后的处理方法
读取文本框中的16进制内容,转换为str类型
:return:
"""# 获取文本框中的内容try:
msg = self.ui.hex_input.toPlainText()
result = binascii.a2b_hex(msg.replace(" ", "").lower()).decode()
self.ui.hex_tran_result.setText(result)except:
self.ui.hex_tran_result.setText('输入内容有误,请检查!!!')
3.协议生成和伪造包发送区块
- 按钮点击事件绑定
# 绑定发送一次按钮事件
self.ui.once_send_btn.clicked.connect(self.on_once_send_btn_click)
# 绑定循环开始按钮事件
self.ui.loop_send_begin_btn.clicked.connect(self.on_loop_send_begin_btn_click)
# 绑定循环结束按钮事件
self.ui.loop_send_stop_btn.clicked.connect(self.on_loop_send_stop_btn_click)
# 绑定生成协议按钮事件
self.ui.creat_proto_btn.clicked.connect(self.on_creat_proto_btn_click)
- 实现点击事件方法
def on_creat_proto_btn_click(self):
"""
点击生成协议按钮之后的处理方法
1.首先判断编号格式是否正确
2.判断参数格式是否正确
3.生成一个实例,然后调用对应的协议编号的encode方法,看是否正确返回
4.将生成的字节流转换成WPE字符串显示格式
:return:
"""
# 判断协议号和协议参数是否有内容 if self.ui.proto_id_line.text() and self.ui.proto_msg_line.toPlainText():
try:
# 获取协议号 proto_id = int(self.ui.proto_id_line.text())
try:
# 获取协议参数 args = eval(self.ui.proto_msg_line.toPlainText())
try:
# 声明对应的协议类,并生成协议实例 exec("from proto_cfg.proto_%d import C%d" % (int(proto_id / 1000), proto_id))
obj = eval('C%d' % proto_id)(*args)
# 调用obj序列化方法,获得字节流 proto, length, buf = obj.encode()
# 将字节流拼接后按照wpe的格式显示到文本显示框 # 字节流转wpe的具体实现方法buf_to_wpe这里就不详细说明了 self.ui.textEdit.setText(buf_to_wpe(proto, length, buf))
except:
out_put_to_log("协议生成失败:如确认填写正确,则可能是参数不符合协议格式,请检查对应的协议格式!")
except:
out_put_to_log("协议生成失败:协议参数不正确,请输入正确的参数")
except:
out_put_to_log("协议生成失败:协议编号格式不正确,请输入正确的数字")
else:
self.ui.textEdit.setText("协议号和协议内容不能为空!!!")def on_once_send_btn_click(self):
"""
点击发送一次按钮之后的处理方法
读取文本框中的内容,转换为bytes,组合成一个完整协议,并发送给服务器
:return:
"""
# 首先从发送框得获取封包内容 try:
data = str_to_buf(self.ui.textEdit.toPlainText())
# 判断agent状态是否正常 if self.agent_server.agent_obj.alive:
# 判断封包内容是否符合正确格式,如果是,则发送,否则,提醒格式不正确 if check_data(data):
self.agent_server.agent_obj.agent_insert_send(data)
else:
out_put_to_log("协议检查失败,填入的协议无法正常解析")
else:
self.ui.textEdit.setText("代理服务未启动,请先启动代理服务!")
except:
out_put_to_log("协议发送失败,填入的协议无法正常解析")def on_loop_send_begin_btn_click(self):
"""
点击循环开始按钮之后的处理方法
读取文本框中的内容,转换为bytes,组合成完整的协议,循环发送给服务器
:return:
"""
# 首先从发送框得获取封包内容 data = str_to_byte(self.ui.textEdit.toPlainText())
# 判断agent状态是否正常 if self.agent_server.agent_obj.alive:
# 检查一下封包内容是否符合正确格式,如果是,则发送,否则提醒格式不正确 if check_data(data):
try:
# 获取发送次数 times = int(self.ui.lineEdit.text())
if times == 0:
# 设置一个打断开关 self.agent_server.agent_obj.loop_send = True
try:
# 获取发包间隔时间 sleep_time = int(self.ui.lineEdit_2.text())
# 设置一个最小间隔时间,防止输入错误 if sleep_time < 10:
sleep_time = 10
# 调用agent_insert_send插包方法 Timer(0, self.agent_server.agent_obj.agent_insert_send, [data, times, sleep_time]).start()
except:
out_put_to_log("请检查间隔填写是否正确!")
except:
out_put_to_log("请检查次数填写是否正确!")
else:
out_put_to_log("协议检查失败,填入的协议无法正常解析")
else:
self.ui.textEdit.setText("代理服务未启动,请先启动代理服务!")def on_loop_send_stop_btn_click(self):
"""
点击循环结束按钮之后的处理方法
停止之前进行的循环发协议
:return:
"""
self.agent_server.agent_obj.loop_send = False
4.常用方法区块
这里主要是自己写了几个比较常用的方法,然后跟按钮点击事件进行绑定,这里就不再详细讲了。
5.协议过程和日志的显示
这个主要是将文本信息 append 到 TextBrowser 控件中,这个很简单,这里也就不再讲了,可以自行百度。
三、使用
工具到这里就基本写完了,在使用的时候,我们需要先在客户端的配置文件里配置一个代理服务器,这个代理服务器的 ip 就是本地 ip127.0.0.1,端口设置为我们预留的一个端口,然后在工具那里配置一个服务器选项,ip 和端口为需要连接的服务器,在使用的时候,先选好工具的服务器,启动服务,然后打开游戏客户端,选择代理服务器(这样客户端在选择对应的服务器时才会与我们的代理创建 socket,而不是直连服务器),这样就可以通过工具中转,连接目标服务器了。
希望本文对你有所帮助~~如果对软件测试、接口测试、自动化测试、性能测试、面试经验交流感兴趣可以私聊我或关注公众号“特斯汀软件测试”。免费领取最新软件测试大厂面试资料和Python自动化、接口、框架搭建学习资料!技术大牛解惑答疑,同行一起交流。
相关推荐
- 服务器用的CPU和个人电脑用的CPU有什么区别?一篇文章告诉你!
-
服务器cpu和普通cpu的区别你的电脑CPU是‘短跑健将’,服务器CPU却是‘铁人三项选手’——它不追求瞬间爆发力,而要7×24小时扛住千军万马的数据洪流!想知道为什么企业机房敢收天价服务费?答案全藏...
- “吃鸡”新版本第1天,玩家进入游戏点击“立即更新”,后悔了!
-
欢迎诸位小伙伴们来到天哥开讲的《和平精英》“精英小课堂”~每逢两三个月,这款游戏就会迎来一次大版本迭代更新,很多朋友会在第一时间更新版本,前往全新的主题模式里一探究竟。不过也有一些老玩家并不会立刻更新...
- 中关村在线·aigo存储杯《无畏契约》全国高校争霸赛招募启事
-
以青春之名,燃电竞之火1赛事背景与宗旨在金秋送爽的9月,芊芊学子们即将回归校园生活。为了给精彩的校园生活锦上添花,由中关村在线与aigo存储联合主办的《无畏契约》全国高校争霸赛正式启幕,旨在为全国高...
- 【生肖狗】9.7-9.10提醒:人算不如天算,转变即是转机
-
九月上旬的风,带着秋意的清爽,也带着几分不可捉摸的变数。对于生肖狗的朋友们来说,9月7日到9月10日这四天,格外需要留意“计划与变化”的碰撞——你们向来习惯提前规划,做事稳妥周全...
- 转转客服IM系统的WebSocket集群架构设计和部署方案
-
本文由转转技术李帅分享,原题“转转客服IM的WebSocket集群部署方案”,下文有修订和重新排版。1、引言转转作为国内头部的二手闲置交易平台,拥有上亿的用户。用户在使用转转app遇到问题时,一般可以...
- 上线3天Steam好评率86%,《时间旅者:重生曙光》开启生存恐怖新篇章
-
这里究竟发生了什么?末日降临,真正的故事悄然启幕。目前,生存恐怖类游戏《时间旅者:重生曙光(Cronos:TheNewDawn)》已在PC(Steam、EpicGamesStore)、P...
- 什么神仙洗衣机让我一天有28小时?拆开松下「大四洗」藏了啥秘密
-
说起家庭洗衣的烦恼,想必很多人都有过类似的经历:贴身内衣要单独洗,宝宝的口水巾得小心呵护,宠物玩具怕藏污纳垢,床单被套又体积庞大,把这些东西混在一起洗担心越洗越脏,分开洗又得反复操作,洗完烘、烘完再洗...
- 爆料人挖出GTA6注册的奇葩域名 延续经典讽刺风格
-
等待《侠盗猎车手6》的日子跨越了数个春秋,在游戏圈期盼着这部可能成为史上最重磅游戏的过程中,每过一段时间就会有些许消息浮出水面。最新线索来自数据挖掘者Tez2在GTA论坛的发现,他可能偶然发现了关于...
- 跟着故事去旅行——读《驼峰间:旅行、探险与征服》
-
作者:郭冰茹《驼峰间》记录了旅行家伊本·白图泰有生之年流传的一则寓言,说一对父子被关进了监狱,有一天儿子问父亲他们每天吃的都是些什么肉,父亲说有牛、羊和骆驼,并且详细地描述了每种动物的特点。但不管父亲...
- 前端工程师需要熟悉的Linux服务器(SSH 终端操作)指令
-
在Linux服务器管理中,SSH(SecureShell)是远程操作的核心工具。以下是SSH终端操作的常用命令和技巧,涵盖连接、文件操作、系统管理等场景:一、SSH连接服务器1.基本连接...
- 跳票6年后,「丝之歌」首发把Steam服务器干爆了 | 玩点好的
-
文丨果脯樱花隧道昨天晚上22点,「鸽」了6年的《空洞骑士:丝之歌》终于上线,算是了却不少玩家的执念。毕竟,这款游戏实在让人等了太多太多年,而且曾有过多次定档后跳票的「案底」,不知道把多少人都整出了P...
- 对标魔兽失败!腾讯版“魔兽”运营一年多后,宣布国际服凉凉
-
大家好,这里是正惊游戏,我是正惊小弟。有很多游戏都想干掉《魔兽世界》,但是大部分魔兽杀手都知道自己不是魔兽的对手,不过是想蹭一下人气而已。腾讯也有一款曾经想对标魔兽的大作,可是上线才一年半国际服就宣布...
- 408 Request Timeout:服务器等待客户端发送请求的时间过长。
-
408RequestTimeout是HTTP状态码之一,表示客户端在发送请求时,服务器等待的时间过长,最终放弃了处理该请求。此问题通常与网络延迟、客户端配置、服务器设置或者应用程序的性能有关...
- 梦幻西游:9.9维护解读,全新时间服锁定129级
-
梦幻西游:9.9维护解读,全新时间服锁定129级9月9日维护解读。1、教师节活动开启,一共7天。挂机,答题,收笔墨纸砚,收海马,搞起来。或者是提前收点家具,教师节期间体力珍贵,家具会涨价。又或者是教师...
- 只是拆掉一面墙,空间就立马大变样,这种设计思路,值得学习
-
你有没有过这样的经历?刚买的房子户型图看起来方方正正,装修完却发现——玄关鞋柜只能塞在角落,进门就撞墙;餐厅正好在过道中间,吃饭像走流程;明明有四个房间,却有一个空着没用,像块食之无味的鸡肋;客餐厅之...
你 发表评论:
欢迎- 一周热门
- 最近发表
-
- 服务器用的CPU和个人电脑用的CPU有什么区别?一篇文章告诉你!
- “吃鸡”新版本第1天,玩家进入游戏点击“立即更新”,后悔了!
- 中关村在线·aigo存储杯《无畏契约》全国高校争霸赛招募启事
- 【生肖狗】9.7-9.10提醒:人算不如天算,转变即是转机
- 转转客服IM系统的WebSocket集群架构设计和部署方案
- 上线3天Steam好评率86%,《时间旅者:重生曙光》开启生存恐怖新篇章
- 什么神仙洗衣机让我一天有28小时?拆开松下「大四洗」藏了啥秘密
- 爆料人挖出GTA6注册的奇葩域名 延续经典讽刺风格
- 跟着故事去旅行——读《驼峰间:旅行、探险与征服》
- 前端工程师需要熟悉的Linux服务器(SSH 终端操作)指令
- 标签列表
-
- 如何绘制折线图 (52)
- javaabstract (48)
- 新浪微博头像 (53)
- grub4dos (66)
- s扫描器 (51)
- httpfile dll (48)
- ps实例教程 (55)
- taskmgr (51)
- s spline (61)
- vnc远程控制 (47)
- 数据丢失 (47)
- wbem (57)
- flac文件 (72)
- 网页制作基础教程 (53)
- 镜像文件刻录 (61)
- ug5 0软件免费下载 (78)
- debian下载 (53)
- ubuntu10 04 (60)
- web qq登录 (59)
- 笔记本变成无线路由 (52)
- flash player 11 4 (50)
- 右键菜单清理 (78)
- cuteftp 注册码 (57)
- ospf协议 (53)
- ms17 010 下载 (60)