modbus_tk 源代码分析

前言

modbus_tcp 协议是工业项目中常见的一种基于 TCP/IP 协议的设备数据交互协议。

作为 TCP/IP 协议的上层协议,modbus_tcp 协议涉及到两个概念:client 和 server。但更标准的叫法应该是 master 和 slave。

  • Slave:TCP/IP 协议中的 server 方
  • Master:TCP/IP 协议中的 client 方

而 modbus_tk 库作为 Python 中著名的 modbus 协议封装模块,其源码值得深入研究。

特别是在对并发量等方面有一定要求的情况下,如果需要在 modbus_tk 模块的基础上进行更进一步的开发,就更应该仔细研究其源代码和实现逻辑。

因此,我写下了这篇文章,希望对你有所帮助。

实例化 TcpMaster 对象

导入 TcpMaster 类:

from modbus_tk.modbus_tcp import TcpMaster

TcpMaster 继承于 Master,在其实例化的时候什么也没做。

class TcpMaster(Master):
def __init__(self, host="127.0.0.1", port=502, timeout_in_sec=5.0):
super(TcpMaster, self).__init__(timeout_in_sec)
self._host = host
self._port = port
self._sock = None

Master 的 __init__() 方法中也没有做什么:

class Master(object):
def __init__(self, timeout_in_sec, hooks=None):
self._timeout = timeout_in_sec
self._verbose = False
self._is_opened = False # 记住 _is_opened 现在为 False

建立 socket 链接

TcpMaster 的父类 Master 提供了 execute 方法,该方法提供以下参数:

self,
slave,
function_code,
starting_address,
quantity_of_x=0,
output_value=0,
data_format="",
expected_length=-1,
write_starting_address_fc23=0,
number_file=None,
pdu="",
returns_raw=False

此方法基本上算该模块的核心,无论是读写线圈、还是读写寄存器等都是调用该方法。

接下来其代码体的具体实现,我们将开始进行逐行分析:

is_read_function = False
nb_of_digits = 0
if number_file is None:
number_file = tuple() self.open()

is_read_function 这里赋值为 False、代表后续在 Master.execute() 方法真正执行前,作者会先认为使用者调用的是 write 方法而非 read 方法。

接下来代码中又调用了 self.open() 方法。 由于实例化 TcpMaster 类时什么也没做, 所以 TCP 链接在此时是还没有建立的,而 self.open() 方法就是创建一个 TCP 的 client 端。

def open(self):
if not self._is_opened: # 在初始化方法中,它默认是 False
self._do_open()
self._is_opened = True

这里执行的 self._do_open() 方法由 TcpMaster 实现:

def _do_open(self):
if self._sock: # 如果 self._sock 不是 None、就将 socket 对象关闭
self._sock.close() # 创建一个 socket 对象,AF_INET 为 IPV4 地址家族
# SOCK_STREAM 即为基于流的协议,也就是 TCP 协议
self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 设置超时时间,即实例化 TcpMaster 传入的值,默认参数为 5
self.set_timeout(self.get_timeout()) # 允许重用地址(解决端口占用问题)
self._sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
call_hooks("modbus_tcp.TcpMaster.before_connect", (self, ))
# 进行链接
self._sock.connect((self._host, self._port))
call_hooks("modbus_tcp.TcpMaster.after_connect", (self, ))

这里的 self.set_timeout 由 TcpMaster 实现:

def set_timeout(self, timeout_in_sec):
super(TcpMaster, self).set_timeout(timeout_in_sec)
if self._sock:
# 注意! 这里如果 timeout_in_sec 等于 0
# 那么该 sock 对象就是一个链接时非阻塞的
# 可用于 I/O 多路复用
self._sock.setblocking(timeout_in_sec > 0) # 如果 timeout_in_sec 为 0,则设置为阻塞的 socket 对象
# timeout 不应该传递负数
if timeout_in_sec:
self._sock.settimeout(timeout_in_sec)

看到这里,我们其实不难猜出 modbus_tk 模块中 TcpMaster 的 Master.execute() 方法其实是能支持 self._sock 异常后的无感重联的。

只需要在 slave 方失联后重新调用一次 TcpMaster._do_open() 方法即可,即可实现无感知的重新链接。

写入多个寄存器

接下来 Master.execute() 方法基本是对 TCP 协议的解包、组包代码,我将具体的组包等过程代码都先给注释掉了:

@threadsafe_function
def execute(
self, slave, function_code, starting_address, quantity_of_x=0, output_value=0, data_format="",
expected_length=-1, write_starting_address_fc23=0, number_file=None, pdu="", returns_raw=False
): is_read_function = False
nb_of_digits = 0
if number_file is None:
number_file = tuple() self.open() if function_code == defines.READ_COILS or function_code == defines.READ_DISCRETE_INPUTS:
pass
elif function_code == defines.READ_INPUT_REGISTERS or function_code == defines.READ_HOLDING_REGISTERS:
pass elif function_code == defines.READ_FILE_RECORD:
pass elif (function_code == defines.WRITE_SINGLE_COIL) or (function_code == defines.WRITE_SINGLE_REGISTER):
pass elif function_code == defines.WRITE_MULTIPLE_COILS:
pass elif function_code == defines.WRITE_MULTIPLE_REGISTERS:
pass elif function_code == defines.READ_EXCEPTION_STATUS:
pass elif function_code == defines.DIAGNOSTIC:
pass elif function_code == defines.READ_WRITE_MULTIPLE_REGISTERS:
pass elif function_code == defines.RAW:
pass elif function_code == defines.DEVICE_INFO:
pass else:
raise ModbusFunctionNotSupportedError("The {0} function code is not supported. ".format(function_code)) query = self._make_query() request = query.build_request(pdu, slave) retval = call_hooks("modbus.Master.before_send", (self, request))
if retval is not None:
request = retval
if self._verbose:
LOGGER.debug(get_log_buffer("-> ", request))
self._send(request) call_hooks("modbus.Master.after_send", (self, )) if slave != 0:
pass

为了能够继续向下分析,我们这里先以写入多个寄存器的逻辑入手接着向下看:

READ_WRITE_MULTIPLE_REGISTERS

其代码为:

elif function_code == defines.WRITE_MULTIPLE_REGISTERS:
# 输出值和 format,如果指定了 format 和输出值,将运行下面的逻辑
if output_value and data_format:
byte_count = struct.calcsize(data_format) # 否则先计算整个 byte 的长度
else:
byte_count = 2 * len(output_value) # 使用 struct 对数据进行转换, 采用大端排列
pdu = struct.pack(">BHHB", function_code, starting_address, byte_count // 2, byte_count) # 输出值和 format,如果指定了 format 和输出值,将运行下面的逻辑
if output_value and data_format:
pdu += struct.pack(data_format, *output_value) # 一般我们不会指定 data_format,所以直接往下看
else:
for j in output_value:
# 若 j 大于 0 fmt 就是 H 否则是 h
fmt = "H" if j >= 0 else "h"
# 继续向 pdu 里加数据
pdu += struct.pack(">" + fmt, j)
data_format = ">HH"
if expected_length < 0:
expected_length = 8

构建数据包

无论是读取、写入线圈或者寄存器,每一个请求都会包含一个 pdu 数据单元。

在 Master.execute() 方法下面,每一种读写操作都会运行 TcpMaster._make_query() 方法:

query = self._make_query()
request = query.build_request(pdu, slave)

下面是 TcpMaster._make_query 的代码:

def _make_query(self):
return TcpQuery()

TcpQuery 属于 Query 的派生类,但 Query 实际上是一个 interface 类,故没有实际代码:

class TcpQuery(Query):

    _last_transaction_id = 0  # 记住这个类属性

    def __init__(self):
super(TcpQuery, self).__init__()
self._request_mbap = TcpMbap()
self._response_mbap = TcpMbap()

TcpMbap 类的实例化过程也非常简单,TcpQuery 中实际上就是封装了一个 request 和 response 而已:

class TcpMbap(object):
def __init__(self):
self.transaction_id = 0
self.protocol_id = 0
self.length = 0
self.unit_id = 0

TcpQuery.build_request() 的实现:

def build_request(self, pdu, slave):
if (slave < 0) or (slave > 255):
raise InvalidArgumentError("{0} Invalid value for slave id".format(slave)) self._request_mbap.length = len(pdu) + 1 # pdu 数据单元的长度 + 1
self._request_mbap.transaction_id = self._get_transaction_id() # 获取一个事务 id
self._request_mbap.unit_id = slave # 站号
mbap = self._request_mbap.pack() # 打包
# mbap 和 pdu 数据单元拼接并返回
# mbap 可以认为是 head 而 pdu 则是 body
return mbap + pdu

TcpQuery._get_transaction_id() 会在每次收发包时,都让事务号自增 1,当事务号增加到 65535 后,置 0:

@threadsafe_function
def _get_transaction_id(self):
if TcpQuery._last_transaction_id < 0xffff: # 65535
TcpQuery._last_transaction_id += 1
else:
TcpQuery._last_transaction_id = 0
return TcpQuery._last_transaction_id

TcpMbap.pack() 方法会将所有 TcpMbap.__init__() 中的实例属性通过 struct 进行封包:

def pack(self):
# transaction_id 事务号
# protocol_id 0
# length pdu 数据单元的长度 + 1
# unit_id 设备站号 slave
return struct.pack(">HHHB", self.transaction_id, self.protocol_id, self.length, self.unit_id)

至此,request 请求已经构建完毕了。

发送请求

让我们接着回到 Master.execute() 方法中:

# call_hooks 实际上是运行钩子函数,在后面会有详细介绍
retval = call_hooks("modbus.Master.before_send", (self, request))
if retval is not None:
request = retval # 是否需要打印更多的日志?这个可以通过 Master.set_verbose() 方法进行设置
# 其默认值为 False
if self._verbose:
LOGGER.debug(get_log_buffer("-> ", request)) # 发送请求
self._send(request) call_hooks("modbus.Master.after_send", (self, ))

在 TcpMaster._send() 方法中:

def _send(self, request):
retval = call_hooks("modbus_tcp.TcpMaster.before_send", (self, request))
if retval is not None:
request = retval
try:
# 刷新 socket 确保链接可用
flush_socket(self._sock, 3)
except Exception as msg:
LOGGER.error('Error while flushing the socket: {0}'.format(msg))
# 异常后、将再次运行 TcpMaster._do_open() 尝试重联
self._do_open() # 若 flush_socket() 函数运行没有抛出异常,则代表链接是可用的。
# 这时候才会发送数据
self._sock.send(request)

flush_socket() 函数非常有趣,它通过 select 模块来不断的轮询监听 sock 对象的可读状态,当可读时会自动读取每一次的 1024 个字节数据并将他们抛弃,这里是为了保持发送数据前的连接状态检测没有异常而做的一步操作:

def flush_socket(socks, lim=0):
# lim 传入的是 3, 代表最多读 3 次
input_socks = [socks] # 做成一个监听列表
cnt = 0 # 当前读取到的次数
while True:
# 放入 可读事件列表、可写事件列表、错误事件列表 及监听对象
# 它会返回一个列表:
# [[r_fd, r_fd], [w_fd, w_fd], [e_fd, e_fd]]
# 而 [0] 则是指只拿到可读的文件描述符列表
# 循环事件时间设置的是 0.0 这代表它将一直阻塞在这里,直到 fd 事件被触发
# 若不为 0,则等待 n 秒,进行下一次的循环
i_socks = select.select(input_socks, input_socks, input_socks, 0.0)[0] # 没有可读的文件描述符,则跳出 while 循环
if len(i_socks) == 0:
break # 若拿到了,就循环得到 socks 进行 recv
# 其实这里应该也可以写成 i_socks[0].recv(1024)
# 因为可读事件文件描述符
for sock in i_socks:
sock.recv(1024) # 超出了最大读取限制, 这里应该代表的是连接断开了
if lim > 0:
cnt += 1
if cnt >= lim:
raise Exception("flush_socket: maximum number of iterations reached")

至此、我们一次完整的组包及发送数据的源码分析就走完了。

解析响应

我们接着来看 Master.execute() 方法中关于解析响应信息的代码:

if slave != 0:
response = self._recv(expected_length)
pass

首先,如果站号不等于 0 就会执行 TcpMaster._recv() 方法:

def _recv(self, expected_length=-1):
# to_data 函数会根据 Python 版本来返回不同的内容
# 若是 Python2 则直接返回 string ''
# 若是 Python3 则会返回一个 bytearray('', 'ascii')
response = to_data('')
length = 255 # 如果 response 小于 255, 则不断的读取
while len(response) < length:
rcv_byte = self._sock.recv(1)
if rcv_byte:
response += rcv_byte
# 在第 6 个字节处、通过 struct.unpack() 进行拆包
if len(response) == 6:
to_be_recv_length = struct.unpack(">HHH", response)[2]
length = to_be_recv_length + 6
else:
break
retval = call_hooks("modbus_tcp.TcpMaster.after_recv", (self, response))
if retval is not None:
return retval
return response

得到 response 后,Master.execute() 方法会开始解析响应信息:

retval = call_hooks("modbus.Master.after_recv", (self, response))
if retval is not None:
response = retval
if self._verbose:
LOGGER.debug(get_log_buffer("<- ", response)) response_pdu = query.parse_response(response)

TcpQuery.parse_response() 方法的代码主要将 mbap 和 pdu 进行分离,并且通过 TcpMbap.unpack() 方法将 mbap 解包并通过 TcpMbap.check_response() 进行数据校验:

def parse_response(self, response):
if len(response) > 6:
# 分别拿到 mbap 和 pdu
mbap, pdu = response[:7], response[7:]
# 解包
self._response_mbap.unpack(mbap)
# 校验数据,传入请求的 mbap 以及 pdu 的长度
self._response_mbap.check_response(self._request_mbap, len(pdu))
# 返回 pdu
return pdu
else:
raise ModbusInvalidResponseError("Response length is only {0} bytes. ".format(len(response)))

TcpMbap.unpack() 方法代码如下,将 _response_mbap 的事务号协议 id 等信息进行更新:

def unpack(self, value):
(self.transaction_id, self.protocol_id, self.length, self.unit_id) = struct.unpack(">HHHB", value)

TcpMbap.check_response() 方法代码如下:

def check_response(self, request_mbap, response_pdu_length):
error_str = self._check_ids(request_mbap)
error_str += self.check_length(response_pdu_length) if len(error_str) > 0:
raise ModbusInvalidMbapError(error_str)

TcpMbap._check_ids() 方法代码如下:

    def _check_ids(self, request_mbap):
# self 是响应体, request_mbap 是请求体
# 对比他们的事务号等信息是否一致,若不一致则会在返回一个 error_str, 该 error_str 会在 TcpMbap.check_response()
# 中被 raise
error_str = "" if request_mbap.transaction_id != self.transaction_id:
error_str += "Invalid transaction id: request={0} - response={1}. ".format(
request_mbap.transaction_id, self.transaction_id) if request_mbap.protocol_id != self.protocol_id:
error_str += "Invalid protocol id: request={0} - response={1}. ".format(
request_mbap.protocol_id, self.protocol_id
) if request_mbap.unit_id != self.unit_id:
error_str += "Invalid unit id: request={0} - response={1}. ".format(request_mbap.unit_id, self.unit_id) return error_str

TcpMbap.check_length() 方法代码如下:

def check_length(self, pdu_length):
# 这里思考 pdu 长度为什么 + 1?
# 因为 response 在 TcpMbap.unpack() 方法中,self.length 是 mbap + pdu 的长度
# 所以这里 pdu_length 长度 + 1 实际上就是指整个 head + body 的长度
following_bytes_length = pdu_length+1 # 判断长度是否相等、若不等可能造成的原因是数据拆包不正确 mbap 长了,或者 pdu 短了
# 这种时候就直接返回一个字符串
# TcpMbap.check_response() 中如果 error_str 的长度大于 0, 就会抛出异常了
if self.length != following_bytes_length:
return "Response length is {0} while receiving {1} bytes. ".format(self.length, following_bytes_length)
return ""

至此,TcpQuery().parse_response() 方法就全部执行完毕了。

Master.execute() 方法中就得到了数据单元 pdu。也就是整个数据体。

我们接着往下看 Master.execute() 方法,其实后面已经没有再深层次调用某些内部代码了,也没有新的 I/O 操作了:


response_pdu = query.parse_response(response) (return_code, byte_2) = struct.unpack(">BB", response_pdu[0:2]) # 如果返回的 code 大于 128,直接报错
if return_code > 0x80:
# the slave has returned an error
exception_code = byte_2
raise ModbusError(exception_code)
else:
# 下面都是解析出一个 body 和一个 data_format
# 分别是 读操作、设备信息、写操作
# 他们所得到的 body 都不一样
if is_read_function:
byte_count = byte_2
data = response_pdu[2:]
if byte_count != len(data):
# the byte count in the pdu is invalid
raise ModbusInvalidResponseError(
"Byte count is {0} while actual number of bytes is {1}. ".format(byte_count, len(data))
)
elif function_code == defines.DEVICE_INFO:
data = response_pdu[1:]
data_format = ">" + (len(data) * "B")
else:
# returns what is returned by the slave after a writing function
data = response_pdu[1:] # 默认为 False
if returns_raw:
return data # 解包,通过 读、写、设备信息所得到的 data_format 和 data
# 对数据进行操作
result = struct.unpack(data_format, data) # 只有 function_code 是 READ_COILS 时,nb_of_digits 才不为 0
if nb_of_digits > 0:
digits = []
for byte_val in result:
for i in range(8):
if len(digits) >= nb_of_digits:
break
digits.append(byte_val % 2)
byte_val = byte_val >> 1
result = tuple(digits) # 如果 function_code 是 READ_FILE_RECORD 读取文件记录,则也需要对 result 进行
# 再次的修改
if function_code == defines.READ_FILE_RECORD:
sub_seq = list()
ptr = 0
while ptr < len(result):
sub_seq += ((ptr + 2, ptr + 2 + result[ptr] // 2), )
ptr += result[ptr] // 2 + 2
result = tuple(map(lambda sub_seq_x: result[sub_seq_x[0]:sub_seq_x[1]], sub_seq)) # 返回 result
return result

threadsafe 装饰器

threadsafe 是一个装饰器函数,在 Master.execute() 方法头上和 TcpQuery._get_transaction_id() 方法头上都加了这个装饰器。

见名知意,该装饰器的主要目的就是为了保障线程安全(有的设备可能不支持同时对其进行读写操作),但是该装饰器也可能会带来另一些问题。

我们先看它的源码:

def threadsafe_function(fcn):
# 实例化出了一把递归锁
lock = threading.RLock() def new(*args, **kwargs):
# 当 Master.execute() 和 TcpQuery._get_transaction_id() 方法没有通过
# 关键字传参传入 threadsafe=False 时,将默认开启线程安全模式来执行
# 这 2 个方法
threadsafe = kwargs.pop('threadsafe', True)
if threadsafe:
lock.acquire()
try:
ret = fcn(*args, **kwargs)
except Exception as excpt:
raise excpt
finally:
if threadsafe:
lock.release()
return ret
return new

这个 threading lock 会导致什么问题呢?当 Python 解释器运行到 Master.execute() 方法头上时,就会自动执行该装饰器。

而 lock 变量也就生成了,最后会返回内部闭函数 new()。

可以理解为这个 lock 已经被当成了一个全局变量,后续无论是创建多少个 TcpMaster 的实例对象,lock 变量所指向的锁都是同一个。

通过源码分析我们得知,Master.execute() 方法中会去建立 socket 链接,一旦有 1 个 device 链接时间过长,也将会导致其他的 device 通信或链接阻塞。

因为它们都是用的同一个 lock 锁。所以,一般来说在使用时我们会在 Master.execute() 方法中显式的传递 threadsafe=False 的关键字参数,自己实现 lock 来解决同一 device 不能同时读写的问题。

一些 hooks 钩子函数

在上面分析源码时,我们会看到很多 call_hooks 的运行,他们其实是 modbus_tk 模块所提供的钩子函数。只要实现相应的钩子函数就会在整个 modbus_tcp 的数据传递生命周期中自动运行。

以下是常见的钩子函数:

def install_hook(name, fct):
"""
Install one of the following hook modbus_rtu.RtuMaster.before_open((master,))
modbus_rtu.RtuMaster.after_close((master,)
modbus_rtu.RtuMaster.before_send((master, request)) returns modified request or None
modbus_rtu.RtuMaster.after_recv((master, response)) returns modified response or None modbus_rtu.RtuServer.before_close((server, ))
modbus_rtu.RtuServer.after_close((server, ))
modbus_rtu.RtuServer.before_open((server, ))
modbus_rtu.RtuServer.after_open(((server, ))
modbus_rtu.RtuServer.after_read((server, request)) returns modified request or None
modbus_rtu.RtuServer.before_write((server, response)) returns modified response or None
modbus_rtu.RtuServer.after_write((server, response))
modbus_rtu.RtuServer.on_error((server, excpt)) modbus_tcp.TcpMaster.before_connect((master, ))
modbus_tcp.TcpMaster.after_connect((master, ))
modbus_tcp.TcpMaster.before_close((master, ))
modbus_tcp.TcpMaster.after_close((master, ))
modbus_tcp.TcpMaster.before_send((master, request))
modbus_tcp.TcpServer.after_send((master, request))
modbus_tcp.TcpMaster.after_recv((master, response)) modbus_tcp.TcpServer.on_connect((server, client, address))
modbus_tcp.TcpServer.on_disconnect((server, sock))
modbus_tcp.TcpServer.after_recv((server, sock, request)) returns modified request or None
modbus_tcp.TcpServer.before_send((server, sock, response)) returns modified response or None
modbus_tcp.TcpServer.on_error((server, sock, excpt)) modbus_rtu_over_tcp.RtuOverTcpMaster.after_recv((master, response)) modbus.Master.before_send((master, request)) returns modified request or None
modbus.Master.after_send((master))
modbus.Master.after_recv((master, response)) returns modified response or None modbus.Slave.handle_request((slave, request_pdu)) returns modified response or None
modbus.Slave.handle_write_multiple_coils_request((slave, request_pdu))
modbus.Slave.handle_write_multiple_registers_request((slave, request_pdu)) returns modified response or None
modbus.Slave.handle_write_single_register_request((slave, request_pdu)) returns modified response or None
modbus.Slave.handle_write_single_coil_request((slave, request_pdu)) returns modified response or None
modbus.Slave.handle_read_input_registers_request((slave, request_pdu)) returns modified response or None
modbus.Slave.handle_read_holding_registers_request((slave, request_pdu)) returns modified response or None
modbus.Slave.handle_read_discrete_inputs_request((slave, request_pdu)) returns modified response or None
modbus.Slave.handle_read_coils_request((slave, request_pdu)) returns modified response or None
modbus.Slave.handle_read_write_multiple_registers_request((slave, request_pdu)) returns modified response or None
modbus.Slave.handle_read_exception_status_request((slave, request_pdu)) returns modified response or None modbus.Slave.on_handle_broadcast((slave, response_pdu)) returns modified response or None
modbus.Slave.on_exception((slave, function_code, excpt)) modbus.Databank.on_error((db, excpt, request_pdu)) modbus.ModbusBlock.setitem((self, slice, value)) modbus.Server.before_handle_request((server, request)) returns modified request or None
modbus.Server.after_handle_request((server, response)) returns modified response or None
modbus.Server.on_exception((server, excpt))
"""
with _LOCK:
try:
_HOOKS[name].append(fct)
except KeyError:
_HOOKS[name] = [fct]

Python modbus_tk 库源码分析的更多相关文章

  1. Python 进阶之源码分析:如何将一个类方法变为多个方法?

    前一篇文章<Python 中如何实现参数化测试?>中,我提到了在 Python 中实现参数化测试的几个库,并留下一个问题: 它们是如何做到把一个方法变成多个方法,并且将每个方法与相应的参数 ...

  2. cJSON库源码分析

    本文采用以下协议进行授权: 自由转载-非衍生-保持署名|Creative Commons BY-NC-ND 3.0 ,转载请注明作者及出处. cJSON是一个超轻巧,携带方便,单文件,简单的可以作为A ...

  3. Redis网络库源码分析(1)之介绍篇

    一.前言 Redis网络库是一个单线程EPOLL模型的网络库,和Memcached使用的libevent相比,它没有那么庞大,代码一共2000多行,因此比较容易分析.其实网上已经有非常多有关这个网络库 ...

  4. springBoot集成Redis遇到的坑(择库)源码分析为什么择库失败

    提示: springboot提供了一套链接redis的api,也就是个jar包,用到的连接类叫做LettuceConnectionConfiguration,所以我们引入pom时是这样的 <de ...

  5. Python之socketserver源码分析

    一.socketserver简介 socketserver是一个创建服务器的框架,封装了许多功能用来处理来自客户端的请求,简化了自己写服务端代码.比如说对于基本的套接字服务器(socket-based ...

  6. Python之namedtuple源码分析

    namedtuple()函数根据提供的参数创建一个新类,这个类会有一个类名,一些字段名和一个可选的用于定义类行为的关键字,具体实现如下 namedtuple函数源码 from keyword impo ...

  7. 经典iOS第三方库源码分析 - YYModel

    YYModel介绍 YYModel是一个针对iOS/OSX平台的高性能的Model解析库,是属于YYKit的一个组件,创建是ibireme. 其实在YYModel出现之前,已经有非常多的Model解析 ...

  8. AspNetCore.AsyncInitialization库源码分析

    AspNetCore.AsyncInitialization 这个库是用来实现在asp.net core应用程序启动时异步执行异步任务.可参考:如何在ASP.NET Core程序启动时运行异步任务(2 ...

  9. Redis事件库源码分析

    由于老大在新项目中使用redis的事件库代替了libevent,我也趁着机会读了一遍redis的事件库代码,第一次读到“优美,让人愉快”的代码,加之用xmind制作的类图非常帅,所以留文纪念. Red ...

  10. Redis网络库源码分析(3)之ae.c

    一.aeCreateEventLoop & aeCreateFileEvent 上一篇文章中,我们已经将服务器启动,只是其中有些细节我们跳过了,比如aeCreateEventLoop函数到底做 ...

随机推荐

  1. 团队如何选择合适的Git分支策略?

    现代软件开发过程中要实现高效的团队协作,需要使用代码分支管理工具实现代码的共享.追溯.回滚及维护等功能.目前流行的代码管理工具,包括CVS,SVN,Git,Mercurial等. 相比CVS和SVN的 ...

  2. 开发者需掌握的超实用VS Code for Windows快捷键

    链接|https://dev.to/devland/100-crucial-keyboard-shortcuts-for-vs-code-users-4474 作者|Thomas Sentre 翻译| ...

  3. 图与网络分析—R实现(四)

    三 最短路问题 最短路问题(short-path problem)是图论理论的一个经典问题.寻找最短路径就是在指定网络中两结点间找一条距离最小的路.最短路不仅仅指一般地理意义上的距离最短,还可以引申到 ...

  4. 【CTF】关于 .init .fini .init_array .fini_array 日志 2019.7.16 pwn

    查找资料的高效性 retn 返回到栈顶地址 关于 .init .fini .init_array .fini_array 其中存放着的是在main函数执行前执行的代码,由__libc_start_ma ...

  5. [MAUI 项目实战] 手势控制音乐播放器(一): 概述与架构

    这是一篇系列博文.请关注我,学习更多.NET MAUI开发知识! [MAUI 项目实战] 手势控制音乐播放器(一): 概述与架构 [MAUI 项目实战] 手势控制音乐播放器(二): 手势交互 [MAU ...

  6. TS(二)内置对象与class类

    内置对象 ECMA内置对象 Boolean.Number.String.RegExp.Date.Error const regexp:RegExp = /\w\d\s const number:Num ...

  7. .NET敏捷开发框架-RDIFramework.NET V5.1发布(跨平台)

    RDIFramework.NET,基于全新.NET Framework与.NET Core的快速信息化系统敏捷开发.整合框架,给用户和开发者最佳的.Net框架部署方案.为企业快速构建跨平台.企业级的应 ...

  8. Python全栈开发工程师 day57 jQuery

    二.jQuery样式操作标签样式操作<!DOCTYPE html><html lang="en"><head> <meta charset ...

  9. shell脚本编程(一)

    c81ba641-5ed7-4ab9-a7c0-e319e0f3890b 初识shell脚本编程 最近项目需求,需要了解下shell脚本编程,所以自己就必须玩玩了= = 初识shell脚本编程,找了几 ...

  10. go slice使用

    1. 简介 在go中,slice是一种动态数组类型,其底层实现中使用了数组.slice有以下特点: *slice本身并不是数组,它只是一个引用类型,包含了一个指向底层数组的指针,以及长度和容量. *s ...