编写一个socket通信客户端,首先要做的是确定I/O模型。这决定了程序的性能和试用场景,也决定了程序的大致框架。
下图来自《Unix网络编程》,直观地比较了各种I/O模型间的特点。
- 阻塞式I/O
收发消息过程中,若没有完成消息接受/发送工作,则程序阻塞在recv()/send()函数处。 - 非阻塞式I/O
收发消息过程中,若没有完成消息接受/发送工作,则recv()/send()立即返回,通过全局变量errno返回此时连接状态。但这种方式耗费cpu资源较高,不推荐使用。另外,如果单纯地想检测连接状态,可以使用select()函数。 - I/O复用
主要用在高并发场合。 - 信号驱动I/O
由SIGIO信号实现异步操作。不过由于TCP协议产生信号频繁,捕获信号并调用相应函数的意义不大,通常用于UDP应用。 - 异步I/O
只有这种I/O模型是真正意义上的异步I/O,有待学习。
以下是一个简单的半双工非阻塞式的socket客户端程序。所谓半双工是指不能同时收/发,非阻塞则指收/发过程不会使请求进程停滞。
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#define BUFSIZE 64 //缓冲区容量
#define CACHECNT 16 //暂存字符串数目
int main()
{
char select,buf[BUFSIZE],cache[CACHECNT][BUFSIZE];
int sockfd,cacheNum = -1;;
if((sockfd = socket(AF_INET,SOCK_STREAM,0)) <0)
{
perror("Create socket failed");
exit(-1);
}
struct sockaddr_in addr;
memset(&addr,0,sizeof(struct sockaddr_in));
char serverIP[15];
int serverPort;
printf("server ip:");
scanf("%s",serverIP);
printf("server port:");
scanf("%d",&serverPort);
addr.sin_family = AF_INET;
addr.sin_port = htons(serverPort);
addr.sin_addr.s_addr = inet_addr(serverIP);
if(connect(sockfd,(struct sockaddr *)(&addr),sizeof(struct sockaddr)) < 0)
{
perror("Connection error!");
exit(-1);
}
else
printf("Connected:%s:%d",serverIP,serverPort);
fcntl(sockfd,F_SETFL,O_NONBLOCK);//设置socket非阻塞模式
for(;;)
{
getchar();
puts("\n1.Receive msg\n2.Save file\n3.Send msg\n4.Send file\n0.Exit\n");
printf("[command]");
scanf("%c",&select);
switch(select)
{
case '0':exit(0);break;
case '1':
{
memset(buf,0,sizeof(buf));
if(recv(sockfd,buf,sizeof(buf),0) <=0)
{
perror("[error]Recv failed");
break;
}
else
{
cacheNum ++;
strcpy(cache[cacheNum],buf);
printf("[recv][%d]%s\n",cacheNum+1,cache[cacheNum]);
if(cacheNum == CACHECNT)
{
puts("[error]The buffer is full,please save into file!");
break;
}
}
}break;
case '2':
{
if(cacheNum == -1)
{
puts("[error]The buffer is empty!");
break;
}
char filePath[BUFSIZE];
printf("[path]");
scanf("%s",filePath);
FILE *fp;
fp = fopen(filePath,"at");
if(fp == NULL)
{
perror("[error]Open file failed");
break;
}
else
{
int i;
for(i = 0;i <= cacheNum;i++)
{
if(fputs(cache[i],fp) == EOF)
{
perror("[error]Write file failed");
break;
}
else
{
fputs("\n",fp);
printf("[write]%s\n",cache[i]);
memset(cache[i],0,sizeof(cache[i]));
}
}
cacheNum = -1;//清空接收缓存
fflush(fp);
fclose(fp);
}
}break;
case '3':
{
memset(buf,0,sizeof(buf));
printf("[send]");
gets(buf);
if(send(sockfd,buf,BUFSIZE,0) == -1)
{
perror("send failed");
break;
}
else
{
printf("-->OK\n");
}
}break;
case '4':
{
char readBuf[BUFSIZE],filePath[BUFSIZE];
printf("[path]");
scanf("%s",filePath);
FILE *fp = NULL;
fp = fopen(filePath,"r");
if(fp == NULL)
{
perror("read file failed");
break;
}
else
{
/*按行读取文件,每行发送一次,发送格式为#BEGIN#-data-#EOF#*/
send(sockfd,"#BEGIN#",8,0);
while((fgets(readBuf,BUFSIZE,fp))!=NULL)
{
send(sockfd,readBuf,BUFSIZE,0);
printf("[send]%s",readBuf);
usleep(200000);
}
memset(buf,0,sizeof(buf));
send(sockfd,"#EOF#",6,0);
fclose(fp);
}
}break;
default:break;
}
}
return 0;
}
程序运行截图如图所示,有简单的交互界面,实现了消息和文本文件的收发功能。
由于使用了非阻塞式通信方式,在服务端发送消息后,客户端必须手动进入选项1才能接受消息。如果这期间服务端发来多条消息,则都会进入缓冲区,并合并输出。