参加这个比赛的过程中,自己也摸索和学习了很多新的知识,特别是网络编程这块,以前学得太简单,这次到了前所未有的学习深度。这里一并总结一下。
技术要点:
1.C语言实现简单数据结构。
由于大赛要求必须在c环境下,而且不允许用第三方库,所以必须在编码实现方案之前,实现一些可能用到的数据结构,包括双链表,map。
1.1 简单双链表的实现。
我这里参考了linux内核中链表的实现来实现简单双链表。
链表节点定义:
/*链表节点*/
typedef struct _ListNode
{
struct _ListNode* next;//链表上的下游节点
struct _ListNode* pre;//链表上的上游节点
}ListNode;
整数链表节点定义:
/*整数链表节点*/
typedef struct _IntNode
{
ListNode listCtrl;//必须在结构体的最前面
int data;//数据域
}IntNode;
链表遍历:
/*遍历链表。pos为遍历游标,head为需要遍历的链表*/
#define for_each(pos,head) \
for(pos=(head)->next;pos!=(head);pos=pos->next) \
/*安全遍历链表。pos为遍历游标,n为临时变量,head为表头。*/
#define for_each_safe(pos,n,head) \
for(pos=(head)->next;n=pos->next,pos!=(head);pos=n) \
1.2 map的实现。
map其实就是存储key-value对。我这里直接在简单链表的基础上实现map:简单链表上每个节点有两个整数key和value。节点定义:
/*map节点*/
typedef struct _MapNode
{
ListNode listCtrl;//用双链表来组织map节点
int key;//键
int value;//值
}MapNode;
2。 日志模块
在设计方案时,考虑到网络编程复杂性,设计了一个日志模块辅助开发和调试。日志模块的实现很简单,参考printf函数的实现实现一个自己的日志打印函数LogPrintf:
int LogPrintf(const char* fmt,...)
{
int ret = 0;
va_list args;
//解析输入格式
va_start(args, fmt);
//如果指定了日志文件,则将日志输出到日志文件。
if(gLogFile)
{
ret = vfprintf(gLogFile,fmt,args);
//及时将日志内容刷到文件。
fflush(gLogFile);
return ret;
}
else
{
//将日志信息输出到屏幕
return vfprintf(stdout,fmt,args);
}
}
3。快速刷新控制台
本比赛中没有设计GUI界面,用了一个cmd终端来做界面。于是遇到了一个快速高效刷新控制台的问题,解决办法很简单,在用printf函数前将光标定位,这样可以将需要变化的字符刷新即可,其他不需要变化的字符,如界面的label,就不需要刷新,其实就是局部刷新的意思啦。
vc下光标定位函数:
void gotoxy(int x,int y){
COORD coord;
coord.X=x;
coord.Y=y;
SetConsoleCursorPosition( GetStdHandle( STD_OUTPUT_HANDLE ), coord );
}
vc下某处打印字符串函数:
void printfString(int x,int y,int totalLen,int type,const char* fmt,...)
{
char temp[100];
va_list args;
//解析输出格式
va_start(args, fmt);
gotoxy(x,y);
//将信息输出到字符数组
vsprintf(temp,fmt,args);
getSpaceString(temp,totalLen,type);
printf("%s",buffer);
//gotoxy(0,23);
}
4.setsockopt相关
地址重用:
setsockopt(gLocalSocket,SOL_SOCKET,SO_REUSEADDR,(const char*)&bReuseaddr,sizeof( BOOL));
收发超时:
//设置接收超时
setsockopt(sock,SOL_SOCKET,SO_RCVTIMEO,(char *)&recvTm,sizeof(int));
recvTm = SOCKET_TIMEOUT_MS;
//设置发送超时
setsockopt(sock,SOL_SOCKET,SO_SNDTIMEO,(char *)&recvTm,sizeof(int));
设置阻塞非阻塞:
void SetBlock(SOCKET sock,int blockFlag)
{
//设置非阻塞
unsigned long ul = (blockFlag==FALSE) ? 1:0; //1为非阻塞,0为阻塞
int ret;
ret = ioctlsocket(sock, FIONBIO, (unsigned long*)&ul);
if(ret==SOCKET_ERROR){
LogPrintf("set %s faield\n",blockFlag==FALSE?"unblock":"block");
}
}
异步连接快速判断是否可以连接服务器:
//建立和服务器的连接
if(connect(sock,(struct sockaddr*)&serverAddress,sizeof(serverAddress)) == SOCKET_ERROR)
{
errorCode = GetLastError();
//sock为非法socket 则重新创建socket
if(errorCode == WSAENOTSOCK)
{
while((sock = socket(AF_INET,SOCK_STREAM,0)) == INVALID_SOCKET);
continue;
}
while(connectRetryCount > 0)
{
FD_ZERO(&fdWrite);
FD_SET(sock,&fdWrite);
errorCode = select(0,NULL,&fdWrite,0,&timeout);
//已经连接上
if(errorCode > 0)
{
SetBlock(sock,TRUE);
return sock;
}
else if(errorCode==0)//连接超时
{
connectRetryCount--;
}
else
{
LogPrintf("NTCreateSocket:select error %d!\n",GetLastError());
}
}
}
else
{
SetBlock(sock,TRUE);
//连接成功,则返回已经连接服务器的socket
return sock;
}
5。网络错误处理
发送数据时,一般需要处理的错误码是WSAENETRESET、WSAECONNRESET、WSAECONNABORTED、WSAETIMEDOUT。
接收数据时,一般需要处理的错误码是WSAEINTR(这种情况属于正常情况)、WSAECONNRESET、WSAETIMEDOUT、 WSAECONNABORTED。