服务器与客户端可以双向收发消息,如果任意一方被外部强制断开,另一方也会退出程序。任意一方输入“quit”并发送,客户端与服务器都会退出。
服务器需要先启动,并且通过主函数传参,输入自己的ip和端口号。客户端也需要通过主函数传参把自己的ip和端口号以及要连接的服务器的ip和端口号传入。
#ifndef _MYHEAD_H
#define _MYHEAD_H
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#endif
#include "myhead.h"
/*
多路复用实现tcp双向通信(模拟客户端)
*/
int main(int argc,char **argv)
{
int tcpsock; //套接字文件描述符
int ret; //返回值
char sbuf[100]; //存放发送的消息的数组
char rbuf[100]; //存放接收的消息的数组
//定义一个文件描述符集合变量并初始化清空
fd_set myset;
FD_ZERO(&myset);
//定义客户端的ipv4地址结构体变量
struct sockaddr_in bindaddr;
bzero(&bindaddr,sizeof(bindaddr));
bindaddr.sin_family = AF_INET;
bindaddr.sin_port = htons(atoi(argv[2])); //自己指定一个端口号
bindaddr.sin_addr.s_addr = inet_addr(argv[1]); //指定自己的ip
//定义服务器的ipv4地址结构体变量
struct sockaddr_in serveraddr;
bzero(&serveraddr,sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(atoi(argv[4])); //服务器端口号
serveraddr.sin_addr.s_addr = inet_addr(argv[3]); //服务器的ip
//创建套接字
tcpsock = socket(AF_INET,SOCK_STREAM,0);
if(tcpsock == -1)
{
perror("无法创建套接字!");
return -1;
}
//绑定ip和端口号
ret = bind(tcpsock,(struct sockaddr *)&bindaddr,sizeof(bindaddr));
if(ret == -1)
{
perror("绑定失败");
return -1;
}
//连接服务器
ret = connect(tcpsock,(struct sockaddr *)&serveraddr,sizeof(serveraddr));
if(ret == -1)
{
perror("连接服务器失败");
return -1;
}
/*
FD_SET(tcpsock,&myset);
FD_SET(0,&myset);
*/
//死循环接收发消息
while(1)
{
//将要监测的(套接字)文件描述符添加进集合变量
FD_SET(tcpsock,&myset);
//将要监测的(键盘)文件描述符添加进集合变量
FD_SET(0,&myset);
//调用select函数去监测文件描述符集合变量
ret = select(tcpsock+1,&myset,NULL,NULL,NULL);
if(ret == -1)
{
perror("监测失败!");
return -1;
}
else if(ret == 0)
{
perror("监测超时!");
return -1;
}
else
{
//判断键盘在不在集合中,在则说明键盘处于读就绪
if(FD_ISSET(0,&myset))
{
bzero(sbuf,100);
scanf("%s",sbuf);
send(tcpsock,sbuf,strlen(sbuf),0);
if(strncmp(sbuf,"quit",4) == 0)
exit(0);
}
//判断套接字描述符在不在集合中,在则说明套接字处于读就绪
if(FD_ISSET(tcpsock,&myset))
{
bzero(rbuf,100);
ret = recv(tcpsock,rbuf,100,0);
if(strncmp(rbuf,"quit",4) == 0)
exit(0);
if(ret == 0) //表示对方断开连接了
{
printf("服务器已断开!即将退出程序!\n");
exit(0);
}
printf("服务器发送过来的信息:%s\n",rbuf);
}
}
}
}
#include "myhead.h"
/*
多线程实现tcp双向通信(模拟服务器)
*/
int main(int argc,char **argv)
{
int tcpsock; //套接字文件描述符(此套接字用于监听和接受连接时)
int newsock; //新的套接字文件描述符(用于与客户端通信)
int ret; //返回值
char sbuf[100]; //存放发送的消息的数组
char rbuf[100]; //存放接收的消息的数组
//定义一个文件描述符集合变量并初始化清空
fd_set myset;
FD_ZERO(&myset);
//定义服务器的ipv4地址结构体变量
struct sockaddr_in bindaddr;
bzero(&bindaddr,sizeof(bindaddr));
bindaddr.sin_family=AF_INET;
bindaddr.sin_port=htons(atoi(argv[2])); //服务器自己的端口号
bindaddr.sin_addr.s_addr=inet_addr(argv[1]); //服务器自己的ip
/*
定义客户端的ipv4地址结构体变量
无需程序员赋值,
accept函数自动存放连接的客户端的ip和端口号
*/
struct sockaddr_in clientaddr;
bzero(&clientaddr,sizeof(clientaddr));
int addrsize=sizeof(clientaddr);
//创建套接字
tcpsock=socket(AF_INET,SOCK_STREAM,0);
if(tcpsock==-1)
{
perror("创建套接字失败!");
return -1;
}
//绑定ip和端口号
ret=bind(tcpsock,(struct sockaddr *)&bindaddr,sizeof(bindaddr));
if(ret==-1)
{
perror("绑定失败");
return -1;
}
//监听
ret=listen(tcpsock,5);
if(ret==-1)
{
perror("监听失败");
return -1;
}
//接受客户端的连接请求
newsock=accept(tcpsock,(struct sockaddr *)&clientaddr,&addrsize);
if(newsock==-1)
{
perror("无法接受客户端的连接请求");
return -1;
}
//打印出连接上服务器的客户端的ip和端口号
printf("ip为%s,端口号为%d的客户端成功连接服务器!\n",inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port));
/*
FD_SET(newsock,&myset);
FD_SET(0,&myset); */
//死循环接收发消息
while(1)
{
//将要监测的套接字文件描述符添加进集合变量
FD_SET(newsock,&myset);
//将要监测的键盘文件描述符添加进集合变量
FD_SET(0,&myset);
//调用select函数去监测集合变量
ret = select(newsock+1,&myset,NULL,NULL,NULL);
if(ret == -1)
{
perror("监测失败!");
return -1;
}
else if(ret == 0)
{
perror("监测超时!");
return -1;
}
else
{
//判断新的套接字文件描述符在不在集合中,在则说明套接字处于读就绪
if(FD_ISSET(newsock,&myset))
{
bzero(rbuf,100);
ret=recv(newsock,rbuf,100,0); //一定不能用旧的套接字
if(strncmp(rbuf,"quit",4) == 0)
exit(0);
if(ret==0) //表示对方断开连接了
{
printf("客户端已断开!即将退出!\n");
exit(0);
}
printf("客户端发送过来的信息:%s\n",rbuf);
}
//判断键盘在不在集合中,在则说明键盘处于读就绪
if(FD_ISSET(0,&myset))
{
bzero(sbuf,100);
scanf("%s",sbuf);
send(newsock,sbuf,strlen(sbuf),0);
if(strncmp(sbuf,"quit",4) == 0)
exit(0);
}
}
}
}
说说这个accept()函数,它的原型是:
*int accept(int sockfd, struct sockaddr *addr, socklen_t addrlen);
它的第二个参数是一个存放成功连接的客户端的ip和端口号的结构体,注意它是自动存放的(可参考server.c中的第65行代码),不需要程序员自己去写ip和端口号。 最后说一下它的返回值,其是一个int类型的数,失败返回-1,没啥好说的,重点是如果成功呢,实际上如果成功会返回一个文件描述符,该文件描述符是一个新的套接字的文件描述符,一定要区分这个新的套接字与旧的套接字是不一样的,新的套接字是用于与客户端进行通信的,旧的套接字是用在监听和接受连接请求的。
我在学习accept()函数中的过程中也有点迷糊,后来老师说了一个形象的比喻,可以借以理解:假如一个女孩子(服务器)同时接受了5个朋友的连接请求,那么假如A朋友(客户端A)发过来了一条消息,那么这个女孩子该怎么回复A朋友呢?难道用接受连接的手机(套接字)?那这样大家都能看到消息了,显然是不行的,因为大家既然都能通过这部手机连上你,那么大家就是相通的,这时女孩需要另一部手机来回复A朋友,同理,如果,女孩子需要和另外四个人进行通信,那么需要另外四个新的手机,在这其中累计一共有了6个套接字,一个是socket()函数返回的用于监听和接受连接请求的,另外5个分别是用于与5个不同的朋友通信的。也许不是很恰当的说法,但是大概就是那么一个意思。
开头注意要点中的最后一点,FD_SET()到底放在哪呢?我们需要知道的是:当select返回后,会移除除当前文件描述符以外其他所有的文件描述符。好了,答案很明显了。