实现功能:
客户端:<1>从标准输入(键盘)读入信息,然后通过socket发送到服务器端;
<2>接收来自服务器端的信息,并显示到终端里。
服务器端:<1>从标准输入(键盘)读入信息,然后通过socket发送到客户端;
<2>接收来自客户端的信息,并显示到终端里。
分析:
<1>首先需要建立服务器端与客户端之间通信的socket(本文采用TCP协议的socket);
<2>实现从标准输入读入、显示到终端;
<3>通过socket交互数据;
<4>交互数据时,由于编译器对代码的顺序执行,会造成阻塞(具体分析见下文)
实现细节:
<1>建立服务器端与客户端之间通信的socket(本文采用TCP协议的socket)
服务器端:
//定义server_sockfd
int server_sockfd =
socket(AF_INET,SOCK_STREAM, 0);
//定义sockaddr_in
(每一行的具体含义参考之前的文章推送)
struct sockaddr_in
server_sockaddr,client_addr;
socklen_t length =
sizeof(client_addr);
server_sockaddr.sin_family = AF_INET;
server_sockaddr.sin_port = htons(MYPORT); //8887
server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
//调用bind()函数,将上述地址结构和socket绑定在一起
bind(server_sockfd,(struct sockaddr
*)&server_sockaddr,sizeof(server_sockaddr));
//调用listen()函数,使套接字处于被动打开,监听状态
listen(server_sockfd,QUEUE);;
//当有来自客户端的连接请求(connect)时,调用accept()函数,至此完成TCP的三次握手
//注:conn是已连接套接字描述符、上面的server_sockfd是监听套接字描述符
int conn =
accept(server_sockfd, (struct sockaddr*)&client_addr,
&length);
客户端:
///定义sockfd
int sock_cli =
socket(AF_INET,SOCK_STREAM, 0);
///定义sockaddr_in
struct sockaddr_in
servaddr;
memset(&servaddr,
0, sizeof(servaddr));
servaddr.sin_family =
AF_INET;
servaddr.sin_port =
htons(MYPORT); ///服务器端口
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); ///服务器ip
127.0.0.1:,本地回环地址、服务器在本机
///连接服务器
connect(sock_cli,
(struct sockaddr *)&servaddr, sizeof(servaddr)) ;
<2>实现从标准输入读入、显示到终端
从标准输入读入由函数fgets来实现,例如
fgets(sendbuf, sizeof(sendbuf), stdin);
显示到终端由函数fputs来实现,例如
fputs(recvbuf, stdout);
<3>通过socket交互数据
socket将收发数据封装在函数send和recv里,如下:
send(sock_cli, sendbuf,
strlen(sendbuf),0);
recv(sock_cli, recvbuf,
sizeof(recvbuf),0);
<4>交互数据时,由于编译器对代码的顺序执行,会造成阻塞(重点)
当客户端调用fgets函数以后,会阻塞于该函数,直到有键盘输入并且输入完成才返回。试想,如果此时从服务器端发来消息,由于客户端阻塞于fgets函数的调用,是不会去调用recv函数的,因此会忽略来自服务器的消息(该消息并没有被丢弃,而是保存在客户端的接收缓冲区)。
为了解决阻塞的问题,使得客户端或者服务器端能立即反映来自网络的消息以及来自标准输入的消息,不阻塞在任何一个函数,我们可以通过select函数来实现。
select函数允许进程指示内核等待多个事件中的任何一个发生,并且只在有一个或多个事件发生或经历一段指定长度的时间以后才唤醒它。(进程中,所有事件都是通过描述符来标识的,比如fgets的文件描述符、socket的已连接描述符等)
#include
#include
int select(int maxfdp1,fd_set *readset,
fd_set *writeset, fd_set *exceptset, const struct timeval
*timeout);
其中,maxfdp1是该select作用的所有描述符个数+1;参数readset、writeset、exceptset指定让内核测试读、写、异常的描述符;timeout则是指定一段时间,如果在这段时间内没有任何描述符打开,那么也会返回select的调用,上述参数除了maxfdp1必须指定,其余若不需要可直接置为NULL
服务器端以及客户端信息交互时select的应用:
fd_set rset;
FD_ZERO(&rset);
FD_SET(fileno(stdin),&rset);
FD_SET(sock_cli,&rset);
maxfdp1=5;
select(maxfdp1,&rset,NULL,NULL,NULL);
if(FD_ISSET(fileno(stdin),&rset)){
if(fgets(sendbuf,
sizeof(sendbuf), stdin) != NULL)
send(sock_cli, sendbuf,
strlen(sendbuf),0); ///发送
if(strcmp(sendbuf,"exit\n")==0) break;
memset(sendbuf, 0,
sizeof(sendbuf));
}
if(FD_ISSET(sock_cli,&rset)){
memset(recvbuf,0,sizeof(recvbuf));
int len =
recv(sock_cli, recvbuf, sizeof(recvbuf),0);
if(strcmp(recvbuf,"exit\n")==0) break;
fputs(recvbuf,
stdout);
}