到服务器,首先封装要发送的数据报文,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客户端的几个关键步骤:
- 调用socket函数,申请一个数据流(tcp)类型的socket;
- connect()函数建立tcp连接;
- recv()函数等待接收服务器下发的应答及控制报文;
- send()函数将封装好的数据报文发送给服务器;
- 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(nRcvB