4. tcp客户端

在协议栈源码工程下,存在一个用vs2015建立的TcpServerForStackTesting工程。其运行在windows平台下,模拟实际应用场景下的tcp服务器。当tcp客户端连接到服务器后,服务器会立即下发一个1100多字节长度的控制报文到客户端。之后在整个tcp链路存续期间,服务器会每隔一段随机的时间(90秒到120秒之间)下发控制报文到客户端,模拟实际应用场景下服务器主动下发指令、数据到客户端的情形。客户端则连续上发数据报文到服务器,服务器回馈一个应答报文给客户端。客户端如果收不到该应答报文则会立即重发,直至收到应答报文或超过重试次数后重连服务器。总之,整个测试场景的设计目标就是完全契合常见的商业应用需求,以此来验证协议栈的核心功能指标是否完全达标。用vs2015打开这个工程,配置管理器指定目标平台为x64。main.cpp文件的头部定义了服务器的端口号以及报文长度等信息:

#define SRV_PORT         6410 //* 服务器端口
#define LISTEN_NUM 10 //* 最大监听数
#define RCV_BUF_SIZE 2048 //* 接收缓冲区容量
#define PKT_DATA_LEN_MAX 1200 //* 报文携带的数据最大长度,凡是超过这个长度的报文都将被丢弃

我们可以依据实际情形调整上述配置并利用这个模拟服务器测试tcp客户端的通讯功能。

……
#include "onps.h" #define PKT_FLAG 0xEE //* 通讯报文的头部和尾部标志
typedef struct _ST_COMMUPKT_HDR_ { //* 数据及控制指令报文头部结构
CHAR bFlag; //* 报文头部标志,其值参看PKT_FLAG宏
CHAR bCmd; //* 指令,0为数据报文,1为控制指令报文
CHAR bLinkIdx; //* tcp链路标识,当存在多个tcp链路时,该字段用于标识这是哪一个链路
UINT unSeqNum; //* 报文序号
UINT unTimestamp; //* 报文被发送时刻的unix时间戳
USHORT usDataLen; //* 携带的数据长度
USHORT usChechsum; //* 校验和(crc16),覆盖除头部和尾部标志字符串之外的所有字段
} PACKED ST_COMMUPKT_HDR, *PST_COMMUPKT_HDR; typedef struct _ST_COMMUPKT_ACK_ { //* 数据即控制指令应答报文结构
ST_COMMUPKT_HDR stHdr; //* 报文头
UINT unTimestamp; //* unix时间戳,其值为被应答报文携带的时间戳
CHAR bLinkIdx; //* tcp链路标识,其值为被应答报文携带的链路标识
CHAR bTail; //* 报文尾部标志,其值参看PKT_FLAG宏
} PACKED ST_COMMUPKT_ACK, *PST_COMMUPKT_ACK; //* 提前申请一块静态存储时期的缓冲区用于tcp客户端的接收和发送,因为接收和发送的报文都比较大,所以不使用动态申请的方式
#define RCV_BUF_SIZE 1300 //* 接收缓冲区容量
#define PKT_DATA_LEN_MAX 1200 //* 报文携带的数据最大长度,凡是超过这个长度的报文都将被丢弃
static UCHAR l_ubaRcvBuf[RCV_BUF_SIZE]; //* 接收缓冲区
static UCHAR l_ubaSndBuf[sizeof(ST_COMMUPKT_HDR) + PKT_DATA_LEN_MAX]; //* 发送缓冲区,ST_COMMUPKT_HDR为通讯报文头部结构体
int main(void)
{
EN_ONPSERR enErr;
SOCKET hSocket = INVALID_SOCKET; if(open_npstack_load(&enErr))
{
printf("The open source network protocol stack (ver %s) is loaded successfully. \r\n", ONPS_VER); //* 协议栈加载成功,在这里初始化ethernet网卡或等待ppp链路就绪
#if 0
emac_init(); //* ethernet网卡初始化函数,并注册网卡到协议栈
#else
while(!netif_is_ready("ppp0")) //* 等待ppp链路建立成功
os_sleep_secs(1);
#endif
}
else
{
printf("The open source network protocol stack failed to load, %s\r\n", onps_error(enErr));
return -1;
} //* 分配一个socket
if(INVALID_SOCKET == (hSocket = socket(AF_INET, SOCK_STREAM, 0, &enErr)))
{
//* 返回了一个无效的socket,打印错误日志
printf("<1>socket() failed, %s\r\n", onps_error(enErr));
return -1;
} //* 连接成功则connect()函数返回0,非0值则连接失败
if(connect(hSocket, "192.168.0.2", 6410, 10))
{
printf("connect 192.168.0.2:6410 failed, %s\r\n", onps_get_last_error(hSocket, NULL));
close(hSocket);
return -1;
} //* 等待接收服务器应答或控制报文的时长(即recv()函数的等待时长),单位:秒。0不等待;大于0等待指定秒数;-1一直
//* 等待直至数据到达或报错。设置成功返回TRUE,否则返回FALSE。这里我们设置recv()函数不等待
//* 注意,只有连接成功后才可设置这个接收等待时长,在这里我们设置接收不等待,recv()函数立即返回,非阻塞型
if(!socket_set_rcv_timeout(hSocket, 0, &enErr))
printf("socket_set_rcv_timeout() failed, %s\r\n", onps_error(enErr)); INT nThIdx = 0;
while(TRUE && nThIdx < 1000)
{
//* 接收,前面已经设置recv()函数不等待,有数据则读取数据后立即返回,无数据则立即返回
INT nRcvBytes = recv(hSocket, ubaRcvBuf, sizeof(ubaRcvBuf));
if(nRcvBytes > 0)
{
//* 收到报文,处理之,报文有两种:一种是应答报文;另一种是服务器主动下发的控制报文
//* 在这里添加你的自定义代码
……
} //* 发送数据报文到服务器,首先封装要发送的数据报文,PST_COMMUPKT_HDR其类型为指向ST_COMMUPKT_HDR结构体的指
//* 针,这个结构体是与TcpServerForStackTesting服务器通讯用的报文头部结构
PST_COMMUPKT_HDR pstHdr = (PST_COMMUPKT_HDR)l_ubaSndBuf;
pstHdr->bFlag = (CHAR)PKT_FLAG;
pstHdr->bCmd = 0x00;
pstHdr->bLinkIdx = (CHAR)nThIdx++;
pstHdr->unSeqNum = unSeqNum;
pstHdr->unTimestamp = time(NULL);
pstHdr->usDataLen = 900; //* 填充随机数据,随机数据长度加ST_COMMUPKT_HDR结构体长度不超过l_ubaSndBuf的长度即可
pstHdr->usChechsum = 0;
pstHdr->usChechsum = crc16(l_ubaSndBuf + sizeof(CHAR), sizeof(ST_COMMUPKT_HDR) - sizeof(CHAR) + 900, 0xFFFF);
l_ubaSndBuf[sizeof(ST_COMMUPKT_HDR) + 900] = PKT_FLAG; //* 发送上面已经封装好的数据报文
INT nPacketLen = sizeof(ST_COMMUPKT_HDR) + pstHdr->usDataLen + 1;
INT nSndBytes = send(hSocket, l_ubaSndBuf, nPacketLen, 3);
if(nSndBytes != nPacketLen) //* 与实际要发送的数据不相等的话就意味着发送失败了
{
printf("<err>sent %d bytes failed, %s\r\n", nPacketLen, onps_get_last_error(hSocket, &enErr)); //* 关闭socket,断开当前tcp连接,释放占用的协议栈资源
close(hSocket);
return -1;
}
} //* 关闭socket,断开当前tcp连接,释放占用的协议栈资源
close(hSocket); return 0;
}

编写tcp客户端的几个关键步骤:

  1. 调用socket函数,申请一个数据流(tcp)类型的socket;
  2. connect()函数建立tcp连接;
  3. recv()函数等待接收服务器下发的应答及控制报文;
  4. send()函数将封装好的数据报文发送给服务器;
  5. close()函数关闭socket,断开当前tcp连接;

真实场景下,单个tcp报文携带的数据长度的上限基本在1K左右。所以,在上面给出的功能测试代码中,单个通讯报文的长度也设定在这个范围内。客户端循环上报服务器的数据报文的长度900多字节,服务器下发开发板的控制报文长度1100多字节。

与传统的socket编程相比,除了上述几个函数的原型与Berkeley sockets标准有细微的差别,在功能及使用方式上没有任何改变。之所以对函数原型进行调整,原因是传统的socket编程模型比较繁琐——特别是阻塞/非阻塞的设计很不简洁,需要一些看起来很“突兀”地额外编码,比如select操作。在设计协议栈的socket模型时,考虑到类似select之类的操作细节完全可以借助rtos的信号量机制将其封装到底层实现,从而达成简化用户编码,让socket编程更加简洁、优雅的目的。因此,最终呈现给用户的协议栈socket模型部分偏离了Berkeley标准。

5. tcp服务器

常见的tcp服务器要完成的工作无外乎就是接受连接请求,接收客户端上发的数据,下发应答或控制报文,清除不活跃的客户端以释放其占用的系统资源。因此,tcp服务器的功能测试代码分为两部分实现:一部分在主线程完成启动tcp服务器、等待接受连接请求这两项工作(为了突出主要步骤,清除不活跃客户端的工作在这里省略);另一部分单独建立一个线程完成读取客户端数据并下发应答报文的工作。

……
#include "onps.h" #define LTCPSRV_PORT 6411 //* tcp测试服务器端口
#define LTCPSRV_BACKLOG_NUM 5 //* 排队等待接受连接请求的客户端数量
static SOCKET l_hSockSrv; //* tcp服务器socket,这是一个静态存储时期的变量,因为服务器数据接收线程也要使用这个变量 //* 启动tcp服务器
SOCKET tcp_server_start(USHORT usSrvPort, USHORT usBacklog)
{
EN_ONPSERR enErr;
SOCKET hSockSrv; do {
//* 申请一个socket
hSockSrv = socket(AF_INET, SOCK_STREAM, 0, &enErr);
if(INVALID_SOCKET == hSockSrv)
break; //* 绑定地址和端口,功能与Berkeley sockets提供的bind()函数相同
if(bind(hSockSrv, NULL, usSrvPort))
break; //* 启动监听,同样与Berkeley sockets提供的listen()函数相同
if(listen(hSockSrv, usBacklog))
break;
return hSockSrv;
} while(FALSE); //* 执行到这里意味着前面出现了错误,无法正常启动tcp服务器了
if(INVALID_SOCKET != hSockSrv)
close(hSockSrv);
printf("%s\r\n", onps_error(enErr)); //* tcp服务器启动失败,返回一个无效的socket句柄
return INVALID_SOCKET;
} //* 完成tcp服务器的数据读取工作
static void THTcpSrvRead(void *pvData)
{
SOCKET hSockClt;
EN_ONPSERR enErr;
INT nRcvBytes;
UCHAR ubaRcvBuf[256]; while(TRUE)
{
//* 等待客户端有新数据到达
hSockClt = tcpsrv_recv_poll(l_hSockSrv, 1, &enErr);
if(INVALID_SOCKET != hSockClt) //* 有效的socket
{
//* 注意这里一定要尽量读取完毕该客户端的所有已到达的数据,因为每个客户端只有新数据到达时才会触发一个信号到用户
//* 层,如果你没有读取完毕就只能等到该客户端送达下一组数据时再读取了,这可能会导致数据处理延迟问题
while(TRUE)
{
//* 读取数据
nRcvBytes = recv(hSockClt, ubaRcvBuf, 256);
if(nRcvBytes > 0)
{
//* 原封不动的回送给客户端,利用回显来模拟服务器回馈应答报文的场景
send(hSockClt, ubaRcvBuf, nRcvBytes, 1);
}
else //* 已经读取完毕
{
if(nRcvBytes < 0)
{
//* 协议栈底层报错,这里需要增加你的容错代码处理这个错误并打印错误信息
printf("%s\r\n", onps_get_last_error(hSocket, NULL));
}
break;
}
}
}
else //* 无效的socket
{
//* 返回一个无效的socket时需要判断是否存在错误,如果不存在则意味着1秒内没有任何数据到达,否则打印这个错误
if(ERRNO != enErr)
{
printf("tcpsrv_recv_poll() failed, %s\r\n", onps_error(enErr));
break;
}
}
}
} int main(void)
{
EN_ONPSERR enErr; if(open_npstack_load(&enErr))
{
printf("The open source network protocol stack (ver %s) is loaded successfully. \r\n", ONPS_VER); //* 协议栈加载成功,在这里初始化ethernet网卡,并注册网卡到协议栈
emac_init();
}
else
{
printf("The open source network protocol stack failed to load, %s\r\n", onps_error(enErr));
return -1;
} //* 启动tcp服务器
l_hSockSrv = tcp_server_start(LTCPSRV_PORT, LTCPSRV_BACKLOG_NUM);
if(INVALID_SOCKET != l_hSockSrv)
{
//* 在这里添加工作线程启动代码,启动tcp服务器数据读取线程THTcpSrvRead
……
} //* 进入主线程的主逻辑处理循环,等待tcp客户端连接请求到来
while(TRUE)
{
//* 接受连接请求
in_addr_t unCltIP;
USHORT usCltPort;
SOCKET hSockClt = accept(l_hSockSrv, &unCltIP, &usCltPort, 1, &enErr);
if(INVALID_SOCKET != hSockClt)
{
//* 在这里你自己的代码处理新到达的客户端
……
}
else
{
printf("accept() failed, %s\r\n", onps_error(enErr));
break;
}
} //* 关闭socket,释放占用的协议栈资源
close(l_hSockSrv); return 0;
}

编写tcp服务器的几个主要步骤:

  1. 调用socket函数,申请一个数据流(tcp)类型的socket;
  2. bind()函数绑定一个ip地址和端口号;
  3. listen()函数启动监听;
  4. accept()函数接受一个tcp连接请求;
  5. 调用tcpsrv_recv_poll()函数利用协议栈提供的poll模型(非传统的select模型)等待客户端数据到达;
  6. 调用recv()函数读取客户端数据并处理之,直至所有数据读取完毕返回第5步,获取下一个已送达数据的客户端socket;
  7. 定期检查不活跃的客户端,调用close()函数关闭tcp链路,释放客户端占用的协议栈资源;

与传统的tcp服务器编程并没有两样。

协议栈实现了一个poll模型用于服务器的数据读取。poll模型利用了rtos的信号量机制。当某个tcp服务器端口有一个或多个客户端有新的数据到达时,协议栈会立即投递一个或多个信号到用户层。注意,协议栈投递信号的数量取决于新数据到达的次数(tcp层每收到一个携带数据的tcp报文记一次),与客户端数量无关。用户通过tcpsrv_recv_poll()函数得到这个信号,并得到最先送达数据的客户端socket,然后读取该客户端送达的数据。注意这里一定要把所有数据读取出来。因为信号被投递的唯一条件就是有新的数据到达。没有信号, tcpsrv_recv_poll()函数无法得到一个有效的客户端socket,那么剩余数据就只能等到该客户端再次送达新数据时再读了。

其实,poll模型的运作机制非常简单。tcp服务器每收到一组新的数据,就会将该数据所属的客户端socket放入接收队列尾部,然后投信号。所以,数据到达、获取socket与投递信号是一系列的连锁反应,且一一对应。tcpsrv_recv_poll()函数则在用户层接着完成连锁反应的后续动作:等信号、摘取接收队列首部节点、取出首部节点保存的socket、返回该socket以告知用户立即读取数据。非常简单明了,没有任何拖泥带水。从这个运作机制我们可以看出:

  1. poll模型的运转效率取决于rtos的信号量处理效率;
  2. tcpsrv_recv_poll()函数每次返回的socket有可能是同一个客户端的,也可能是不同客户端;
  3. 单个客户端已送达的数据长度与信号并不一一对应,一一对应的是该客户端新数据到达的次数与信号投递的次数,所以当数据读取次数小于信号数时,存在读取数据长度为0的情形;
  4. tcpsrv_recv_poll()函数返回有效的sokcet后,尽量读取全部数据到用户层进行处理,否则会出现剩余数据无法读取的情形,如果客户端不再上发新的数据的话;

6. udp通讯

相比tcp,udp通讯功能的实现相对简单很多。为udp绑定一个固定端口其就可以作为服务器使用,反之则作为一个客户端使用。

……
#include "onps.h" #define RUDPSRV_IP "192.168.0.2" //* 远端udp服务器的地址
#define RUDPSRV_PORT 6416 //* 远端udp服务器的端口
#define LUDPSRV_PORT 6415 //* 本地udp服务器的端口 //* udp通讯用缓冲区(接收和发送均使用)
static UCHAR l_ubaUdpBuf[256]; int main(void)
{
EN_ONPSERR enErr;
SOCKET hSocket = INVALID_SOCKET; if(open_npstack_load(&enErr))
{
printf("The open source network protocol stack (ver %s) is loaded successfully. \r\n", ONPS_VER); //* 协议栈加载成功,在这里初始化ethernet网卡或等待ppp链路就绪
#if 0
emac_init(); //* ethernet网卡初始化函数,并注册网卡到协议栈
#else
while(!netif_is_ready("ppp0")) //* 等待ppp链路建立成功
os_sleep_secs(1);
#endif
}
else
{
printf("The open source network protocol stack failed to load, %s\r\n", onps_error(enErr));
return -1;
} //* 分配一个socket
if(INVALID_SOCKET == (hSocket = socket(AF_INET, SOCK_STREAM, 0, &enErr)))
{
//* 返回了一个无效的socket,打印错误日志
printf("<1>socket() failed, %s\r\n", onps_error(enErr));
return -1;
} #if 0
//* 如果是想建立一个udp服务器,这里需要调用bind()函数绑定地址和端口
if(bind(hSocket, NULL, LUDPSRV_PORT))
{
printf("bind() failed, %s\r\n", onps_get_last_error(hSocket, NULL)); //* 关闭socket释放占用的协议栈资源
close(hSocket);
return -1;
}
#else
//* 建立一个udp客户端,在这里可以调用connect()函数绑定一个固定的目标服务器,接下来就可以直接使用send()函数发送
//* 数据,当然在这里你也可以什么都不做(不调用connect()),但接下来你需要使用sendto()函数指定要发送的目标地址
if(connect(hSocket, RUDPSRV_IP, RUDPSRV_PORT, 0))
{
printf("connect %s:%d failed, %s\r\n", RUDPSRV_IP, RUDPSRV_PORT, onps_get_last_error(hSocket, NULL)); //* 关闭socket释放占用的协议栈资源
close(hSocket);
return -1;
}
#endif //* 与tcp客户端测试一样,接收数据之前要设定udp链路的接收等待的时间,单位:秒,这里设定recv()函数等待1秒
if(!socket_set_rcv_timeout(hSocket, 1, &enErr))
printf("socket_set_rcv_timeout() failed, %s\r\n", szNowTime, onps_error(enErr)); INT nCount = 0;
while(TRUE && nCount < 1000)
{
//* 发缓冲区填充一段字符串然后得到其填充长度
sprintf((char *)l_ubaUdpBuf, "U#%d#%d#>1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ", time(NULL), nCount++);
INT nSendDataLen = strlen((const char *)l_ubaUdpBuf); //* 调用send()函数发送数据,如果实际发送长度与字符串长度不相等则说明发送失败
if(nSendDataLen != send(hSocket, l_ubaUdpBuf, nSendDataLen, 0))
printf("send failed, %s\r\n", onps_get_last_error(hSocket, NULL)); //* 接收对端数据之前清0,以便本地能够正确输出收到的对端回馈的字符串
memset(l_ubaUdpBuf, 0, sizeof(l_ubaUdpBuf)); //* 调用recv()函数接收数据,如果想知道对端地址调用recvfrom()函数,在这里recv()函数为阻塞模式,最长阻塞1秒(如果未收到任何udp报文的话)
INT nRcvBytes = recv(hSocket, l_ubaUdpBuf, sizeof(l_ubaUdpBuf));
if(nRcvBytes > 0)
printf("recv %d bytes, Data = <%s>\r\n", nRcvBytes, (const char *)l_ubaUdpBuf);
else
{
//* 小于0则意味着recv()函数报错
if(nRcvBytes < 0)
{
printf("recv failed, %s\r\n", onps_get_last_error(hSocket, NULL)); //* 关闭socket释放占用的协议栈资源
close(hSocket);
break;
}
}
} //* 关闭socket,断开当前tcp连接,释放占用的协议栈资源
close(hSocket); return 0;
}

udp通讯编程依然遵循了传统习惯,主要编程步骤还是那些:

  1. 调用socket函数,申请一个SOCK_DGRAM(udp)类型的socket;
  2. 如果想建立服务器,调用bind()函数;想与单个目标地址通讯,调用connect()函数;与任意目标地址通讯则什么都不用做;
  3. 调用send()或sendto()函数发送udp报文;
  4. 调用recv()或recvfrom()函数接收udp报文;
  5. close()函数关闭socket释放当前占用的协议栈资源;

onps栈使用说明(3)——tcp、udp通讯测试的更多相关文章

  1. JAVA之旅(三十二)——JAVA网络请求,IP地址,TCP/UDP通讯协议概述,Socket,UDP传输,多线程UDP聊天应用

    JAVA之旅(三十二)--JAVA网络请求,IP地址,TCP/UDP通讯协议概述,Socket,UDP传输,多线程UDP聊天应用 GUI写到一半电脑系统挂了,也就算了,最多GUI还有一个提示框和实例, ...

  2. onps栈使用说明(1)——API接口手册

    1. 底层API 由协议栈底层提供的api,用于涉及底层操作的一些功能实现,这些api接口函数的原型定义分布于不同的文件,它们被统一include进了onps.h中: open_npstack_loa ...

  3. onps栈使用说明(2)——ping、域名解析等网络工具测试

    1. ping测试 协议栈提供ping工具,其头文件为"net_tools/ping.h",将其include进你的目标系统中即可使用这个工具. -- #include " ...

  4. python前后台tcp/udp通讯示例

    以下代码兼容python2.7+.python3 TCP示例 服务器 -- sever_tcp.py #!/usr/bin/env python #coding=utf-8 import time i ...

  5. TCP,UDP 通讯的helper类

    使用Tcp通讯,首先要启动tcp服务端监听客户端,客户端发送消息,服务端收到消息 1.服务端代码如下 public class TcpServerTest { public async Task Be ...

  6. [tools]tcp/udp连通性测试

    一 端口连通性测试意义 测试网络端口可达性,确保给某些使用特定端口的app做链路连通性检测.使它们能够正常的运行起来.   二 法1 使用newclient发包,彼端tcpdump抓包观察是否能收到包 ...

  7. [na][tools]tcp/udp连通性测试

    一 端口连通性测试意义 目的端可以使用nc来临时开一个端口,客户端用telnet来连接测试 测试网络端口可达性,确保给某些使用特定端口的app做链路连通性检测.使它们能够正常的运行起来. 二 测试方法 ...

  8. C#中的TCP通讯与UDP通讯

    最近做了一个项目,主要是给Unity3D和实时数据库做通讯接口.虽然方案一直在变:从开始的UDP通讯变为TCP通讯,然后再变化为UDP通讯;然后通讯的对象又发生改变,由与数据库的驱动进行通讯(主动推送 ...

  9. LWIP裸机环境下实现TCP与UDP通讯

    前面移植了LWIP,并且简单的实用了DHCP的功能,今天来使用一下实际的数据通讯的功能 首先是实现TCP客户端,我先上代码 #ifndef __TCP_CLIENT_H_ #define __TCP_ ...

随机推荐

  1. React报错之Rendered more hooks than during the previous render

    正文从这开始~ 总览 当我们有条件地调用一个钩子或在所有钩子运行之前提前返回时,会产生"Rendered more hooks than during the previous render ...

  2. atcoder beginner contest 251(D-E)

    Tasks - Panasonic Programming Contest 2022(AtCoder Beginner Contest 251)\ D - At Most 3 (Contestant ...

  3. 第七十九篇:数组方法(forEach,some,every,reduce)

    好家伙,来复习几个数组方法, 1.forEach循环与some循环 代码如下: <script> const arr =['奔驰','宝马','GTR','奥迪'] //forEach循环 ...

  4. 高阶 CSS 技巧在复杂动效中的应用

    最近我在 CodePen 上看到了这样一个有意思的动画: 整个动画效果是在一个标签内,借助了 SVG PATH 实现.其核心在于对渐变(Gradient)的究极利用. 完整的代码你可以看看这里 -- ...

  5. Win32简单图形界面程序逆向

    Win32简单图形界面程序逆向 前言 为了了解与学习底层知识,从 汇编开始 -> C语言 -> C++ -> PE文件 ,直至今天的Win32 API,着实学的令我头皮发麻(笑哭). ...

  6. 互联网公司员工职级、研发效能度量、OKR与绩效考核

    今天要写这篇文章,来自最近有两个点触动了我.第一个触动点是奈飞(netflix)做出了一个巨大动作<"不搞职级.人人平等" 25 年后行不通了?Netflix 破天荒引入细分 ...

  7. 网络基础七层模型与TCP/IP协议

    1.网络基础 1.1 什么是网络 网络就是计算机网络是一组计算机或网络设备通过有形 的线缆或无形的媒介如无线,连接起来,按照一定的 规则,进行通信的集合. 网络通信就是指终端设备之间通过计算机网络进行 ...

  8. .NET Core Web APi类库如何内嵌运行?

    话题 我们知道在.NET Framework中可以嵌入运行Web APi,那么在.NET Core(.NET 6+称之为.NET)中如何内嵌运行Web Api呢,在实际项目中这种场景非常常见,那么我们 ...

  9. MySQL8更改数据存储目录

  10. 15. 第十四篇 安装CoreDNS

    文章转载自:https://mp.weixin.qq.com/s?__biz=MzI1MDgwNzQ1MQ==&mid=2247483850&idx=1&sn=4bfdb26f ...