ytes > 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服务器的几个主要步骤:
- 调用socket函数,申请一个数据流(tcp)类型的socket;
- bind()函数绑定一个ip地址和端口号;
- listen()函数启动监听;
- accept()函数接受一个tcp连接请求;
- 调用tcpsrv_recv_poll()函数利用协议栈提供的poll模型(非传统的select模型)等待客户端数据到达;
- 调用recv()函数读取客户端数据并处理之,直至所有数据读取完毕返回第5步,获取下一个已送达数据的客户端socket;
- 定期检查不活跃的客户端,调用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以告知用户立即读取数据。非常简单明了,没有任何拖泥带水。从这个运作机制我们可以看出:
- poll模型的运转效率取决于rtos的信号量处理效率;
- tcpsrv_recv_poll()函数每次返回的socket有可能是同一个客户端的,也可能是不同客户端;
- 单个客户端已送达的数据长度与信号并不一一对应,一一对应的是该客户端新数据到达的次数与信号投递的次数,所以当数据读取次数小于信号数时,存在读取数据长度为0的情形;
- 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(); //* etherne