//注意:本文中服务器模型统一以TCP协议为准
一、服务器模型
在网络通信过程中,服务器端通常需要同时处理多个客户端。由于多个客户端的请求到来的时间不尽相同,服务器端需要处理不同时刻到来的客户端信息。总体上来说,服务器端大致可以使用两种模型来实现:循环服务器与并发服务器。
循环服务器模型处理的手段为“轮询”。当有多个客户端访问服务器时,服务器会按队列顺序依次处理每个客户端的请求,直至前一个客户端请求完全处理完毕,服务器才会继续响应下一个客户端。循环服务器的特点是简单,但是缺点也十分明显:会造成后续客户端等待响应时间过长。
为了提高服务器的处理能力,我们又发明了并发服务器模型。并发服务器模型处理的手段为“并发”,其基本思想是在服务器端采取多任务机制(即多进程或多线程),每个任务分别服务一个客户端。这样极大地提高了服务器的处理能力,但是同时也需要考虑服务器端的进程同步、通信与资源分配。
二、服务器模型——循环服务器
TCP循环服务器是一种比较常见的服务器模型,大致的工作流程如下:
1.服务器端从连接请求队列中提取请求连接的客户端,建立连接并返回新的已连接套接字。
2.服务器端通过已连接套接字接收客户端数据,并处理请求发送给客户端,直至客户端关闭连接。
3.服务器端关闭当前套接字,断开与当前客户端的连接,并返回步骤1。
通过工作流程我们可以发现,循环服务器的本质是采用循环结构处理客户端请求,若当前客户端请求未结束,后续的客户端必须一直等待。循环服务器本质上仍无法同时针对多个客户端进行服务。
//为了构成循环服务器,通常情况下在服务器端的代码内使用二重循环结构
示例:使用循环服务器模型,接收不同客户端的数据,当客户端发送"byebye"时与服务器端断开连接
//服务器端
//文件server.c
//注意:该段代码与TCP编程练习1相似,不过使用了二重循环结构,编写代码时注意区分
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#define BUFFER 128
int main(int argc, const char *argv[])
{
int listenfd,connfd;
struct sockaddr_in servaddr,cliaddr;
socklen_t peerlen;
char buf[BUFFER];
if(argc<3)
{
printf("too few argument\n");
printf("Usage: %s <ip> <port>\n",argv[1]);
exit(0);
}
if((listenfd=socket(AF_INET,SOCK_STREAM,0))<0)
{
perror("socket");
exit(0);
}
printf("listenfd is %d\n",listenfd);
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(atoi(argv[2]));
servaddr.sin_addr.s_addr = inet_addr(argv[1]);
if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
{
perror("bind");
exit(0);
}
printf("bind success!\n");
if(listen(listenfd,10)<0)
{
perror("listen");
exit(0);
}
printf("Listening...\n");
peerlen = sizeof(cliaddr);
while(1)//外层循环控制连接不同的客户端
{
if((connfd=accept(listenfd,(struct sockaddr*)&cliaddr,&peerlen))<0)
{
perror("accept");
exit(0);
}
printf("Connect:[%s:%d]\n",inet_ntoa(cliaddr.sin_addr),ntohs(cliaddr.sin_port));//由于存在多个客户端,因此打印客户端的信息以便区分
while(1)//内层循环控制响应当前已连接客户端的请求
{
memset(buf,0,sizeof(buf));
if(recv(connfd,buf,BUFFER,0)<0)
{
perror("recv");
exit(0);
}
printf("Received a message:%s",buf);
send(connfd,buf,BUFFER,0);//服务器端直接将接收到的信息反射给客户端
if(strncmp(buf,"byebye",6)==0)//指定断开连接信息为"byebye"
{
printf("Disconnect:[%s:%d]\n",inet_ntoa(cliaddr.sin_addr),ntohs(cliaddr.sin_port));
close(connfd);
break;
}
}
}
close(listenfd);
return 0;
}
//客户端
//文件client.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#define BUFFER 128
int main(int argc, const char *argv[])
{
int sockfd;
char buf[BUFFER];
struct sockaddr_in serveraddr;
if(argc<3)
{
printf("too few argument\n");
printf("Usage: %s <ip> <port>\n",argv[0]);
exit(0);
}
if((sockfd = socket(AF_INET,SOCK_STREAM,0))<0)
{
perror("socket");
exit(0);
}
bzero(&serveraddr,sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(atoi(argv[2]));
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
if(connect(sockfd,(struct sockaddr*)&serveraddr,sizeof(serveraddr))<0)
{
perror("connect");
exit(0);
}
while(1)
{
memset(buf,0,sizeof(buf));
fgets(buf,BUFFER,stdin);
send(sockfd,buf,sizeof(buf),0);
if(strncmp(buf,"byebye",6)==0)//指定断开连接信息为"byebye"
{
printf("Client will exit\n");
close(sockfd);
break;
}
memset(buf,0,sizeof(buf));
if(recv(sockfd,buf,BUFFER,0)<0)
{
perror("recv");
exit(0);
}
printf("recv from server: %s",buf);
}
return 0;
}
三、服务器模型——并发服务器
循环服务器模型有许多缺点,例如在某一个时刻内只能为一个客户端服务,因此循环服务器并不能真正达到服务器服务多个客户端的目的。
我们可以使用并发服务器模型完成我们想要的功能。并发服务器模型在网络通信中被广泛应用,它既可以使用多进程实现,也可以采用多线程实现。
若使用进程创建并发服务器,则创建流程如下:
1.服务器端父进程从连接请求队列中提取请求,建立连接并返回已连接的新的套接字
2.服务器端父进程创建子进程为客户端服务,客户端关闭时子进程退出
3.服务器端父进程关闭已连接套接字,返回步骤1
使用线程创建并发服务器的流程与进程类似,创建流程如下:
1.编写线程处理客户端的功能函数
2.主进程从连接请求队列中提取请求,建立连接并创建线程为客户端服务
3.主进程返回步骤1
示例1:使用并发服务器模型,使用进程编程,接收不同客户端的数据,当客户端发送"byebye"时与服务器端断开连接
//服务器端
//文件server_process.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#define BUFFER 128
int main(int argc, const char *argv[])
{
int listenfd,connfd,n;
struct sockaddr_in servaddr,cliaddr;
socklen_t peerlen;
char buf[BUFFER];
pid_t pid;
if(argc<3)
{
printf("too few argument\n");
printf("Usage: %s <ip> <port>\n",argv[1]);
exit(0);
}
if((listenfd=socket(AF_INET,SOCK_STREAM,0))<0)
{
perror("socket");
exit(0);
}
printf("listenfd is %d\n",listenfd);
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(atoi(argv[2]));
servaddr.sin_addr.s_addr = inet_addr(argv[1]);
if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
{
perror("bind");
exit(0);
}
printf("bind success!\n");
if(listen(listenfd,10)<0)
{
perror("listen");
exit(0);
}
printf("Listening...\n");
peerlen = sizeof(cliaddr);
while(1)
{
if((connfd=accept(listenfd,(struct sockaddr*)&cliaddr,&peerlen))<0)
{
perror("accept");
exit(0);
}
printf("Connect:[%s:%d]\n",inet_ntoa(cliaddr.sin_addr),ntohs(cliaddr.sin_port));
pid = fork();
if(pid<0)
{
perror("cannot fork");
exit(0);
}
else if(pid==0)//子进程
{
while(1)
{
if((n=recv(connfd,buf,BUFFER,0))<0)
{
perror("Child Process recv");
exit(0);
}
printf("Received [%s:%d]:%s",inet_ntoa(cliaddr.sin_addr),ntohs(cliaddr.sin_port),buf);
send(connfd,buf,n,0);
if(strncmp(buf,"byebye",6)==0)
{
printf("Disconnect:[%s:%d]\n",inet_ntoa(cliaddr.sin_addr),ntohs(cliaddr.sin_port));
break;
}
}
close(connfd);
exit(0);
}
else//父进程
{
close(connfd);
}
}
close(listenfd);
return 0;
}
//客户端代码同循环服务器
示例2:使用并发服务器模型,使用线程编程,接收不同客户端的数据,当客户端发送"byebye"时与服务器端断开连接
//服务器端
//文件server_thread.c
//注意编译时添加线程库-lpthread
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<pthread.h>
#define BUFFER 128
void *service(void *arg)
{
int newsockfd = *((int*)arg);
char buf[BUFFER];
int n;
printf("Create service thread:%d\n",newsockfd);
while(1)
{
if((n=recv(newsockfd,buf,BUFFER,0))<0)
{
perror("Thread recv");
pthread_exit(NULL);
}
printf("Received string from %d:%s",newsockfd,buf);
send(newsockfd,buf,n,0);
if(strncmp(buf,"byebye",6)==0)
{
printf("Disconnect:%d\n",newsockfd);
break;
}
}
pthread_exit(NULL);
}
int main(int argc, const char *argv[])
{
int listenfd,connfd,n;
struct sockaddr_in servaddr,cliaddr;
socklen_t peerlen;
char buf[BUFFER];
pthread_t tid;
if(argc<3)
{
printf("too few argument\n");
printf("Usage: %s <ip> <port>\n",argv[1]);
exit(0);
}
if((listenfd=socket(AF_INET,SOCK_STREAM,0))<0)
{
perror("socket");
exit(0);
}
printf("listenfd is %d\n",listenfd);
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(atoi(argv[2]));
servaddr.sin_addr.s_addr = inet_addr(argv[1]);
if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
{
perror("bind");
exit(0);
}
printf("bind success!\n");
if(listen(listenfd,10)<0)
{
perror("listen");
exit(0);
}
printf("Listening...\n");
peerlen = sizeof(cliaddr);
while(1)
{
if((connfd=accept(listenfd,(struct sockaddr*)&cliaddr,&peerlen))<0)
{
perror("accept");
exit(0);
}
printf("Connect [%s:%d]\t",inet_ntoa(cliaddr.sin_addr),ntohs(cliaddr.sin_port));
if((pthread_create(&tid,NULL,service,&connfd))!=0)
{
perror("创建线程");
exit(-1);
}
}
close(connfd);
close(listenfd);
return 0;
}
//客户端代码同循环服务器
四、服务器模型——多路复用服务器(选讲)
并发服务器是目前大多数服务器使用的模型,虽然使用简便且功能强大,但是并发服务器模型仍然有许多缺点。例如:
-使用线程的并发服务器在客户端较多时,同时运行的线程较多,会占用大量的存储空间。线程越多,服务器压力越大,执行效率越低
-使用进程的并发服务器需要解决多个进程之间的资源互斥问题,还有需要解决进程间通信问题
为了节省空间资源,我们可以采用多路复用的方式构建服务器,能从一定程度上解决并发服务器在客户端过多时占用大量空间的问题。
//但是由于select()性能较低,因此使用select()构建多路复用服务器并不一定比并发服务器更加高效。采用epoll()函数族构建服务器会更加高效
示例:使用多路复用服务器模型,接收不同客户端的数据,当客户端发送"byebye"时与服务器端断开连接
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/socket.h>
#include<sys/time.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#define BUFFER 128
int main(int argc, const char *argv[])
{
struct stat _stat;//给fstat()传参用
int listenfd,connfd,n,maxfd,i;
fd_set fdset;
struct sockaddr_in servaddr,cliaddr;
socklen_t peerlen;
char buf[BUFFER];
if(argc<3)
{
printf("too few argument\n");
printf("Usage: %s <ip> <port>\n",argv[1]);
exit(0);
}
if((listenfd=socket(AF_INET,SOCK_STREAM,0))<0)
{
perror("socket");
exit(0);
}
printf("listenfd is %d\n",listenfd);
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(atoi(argv[2]));
servaddr.sin_addr.s_addr = inet_addr(argv[1]);
if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
{
perror("bind");
exit(0);
}
printf("bind success!\n");
if(listen(listenfd,10)<0)
{
perror("listen");
exit(0);
}
printf("Listening...\n");
peerlen = sizeof(cliaddr);
//设置多路复用
maxfd = listenfd;
while(1)
{
FD_ZERO(&fdset);//每次select()后都需要重设fdset
for(i=0;i<=maxfd;i++)
{
//fstat()用于检验文件描述符对应的文件是否有效
//因为有些客户端与服务器端断连,因此被close()的文件描述符无法加入到fdset中
if(fstat(i,&_stat)==0)//若文件描述符i有效,即i连接未断开
{
FD_SET(i,&fdset);//将i加入到fdset中
}
}
if(select(maxfd+1,&fdset,NULL,NULL,NULL)<0)//监控fdset中的文件描述符
{
perror("select");
break;
}
for(i=0;i<=maxfd;i++)
{
if(FD_ISSET(i,&fdset))//判断哪个文件描述符被select()
{
if(i==listenfd)//如果是listenfd,则证明有新客户端请求连接服务器
{
if((connfd=accept(i,(struct sockaddr*)&cliaddr,&peerlen))<0)
{
perror("accept");
exit(0);
}
maxfd = maxfd>connfd?maxfd:connfd;//fdset内数量+1
}
else//如果是之前出现过的某个文件描述符i,则证明客户端i正在向客户端发送数据
{
if((n=recv(i,buf,BUFFER,0))<0)
{
perror("recv");
exit(0);
}
printf("Received from %d:%s",i,buf);
send(i,buf,n,0);
if(strncmp(buf,"byebye",6)==0)//接收到byebye后跟客户端断连
{
printf("Disconnect %d\n",i);
close(i);//关闭连接
if(i==maxfd)//若关闭的恰好是最后一个连接的客户端,则fdset总数-1
maxfd--;
}
}
}
}
}
close(listenfd);
return 0;
}
一、套接字超时检测与套接字选项
在网络通信中,经常出现各种不可预知的情况,例如网络线路突发故障、通信一方异常中断(断电、断网等)等情况。一旦出现上述情况,则通信另一方可能会长时间无法收到数据,而且无法判定是没有数据还是数据无法到达。
若通信双方使用TCP协议,由于TCP协议的可靠性,因此当网络连接中断时,send()/recv()会回复错误信息,因此TCP协议可以检测网络连接是否中断。
而对于UDP协议,由于UDP协议的不可靠性,通信双方无法检测网络连接状况,因此需要在程序中设定相关的检测。
在网络通信中设定超时检测是十分必要的,可以避免进程无限制阻塞,当上限时间到时,进程会返回错误并进行后续操作。
若需要检测网络连接超时,需要在创建套接字后设定套接字选项。
//常见套接字选项见
二、套接字超时检测编程
我们可以使用getsockopt()获取套接字的可选项,而使用setsockopt()设定套接字可选项
函数getsockopt()
所需头文件:#include<sys/types.h>
#include<sys/socket.h>
函数原型:int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen)
函数参数:
sockfd 套接字文件描述符
level 选项所属的协议层
optname 选项值
optval 保存选项值的缓冲区
optlen 选项值的长度
函数返回值:
成功:0
失败:-1
函数setsockopt()
所需头文件:#include<sys/types.h>
#include<sys/socket.h>
函数原型:int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen)
函数参数:
sockfd 套接字文件描述符
level 选项所属的协议层
optname 选项值
optval 设置选项值的缓冲区
optlen 选项值的长度
函数返回值:
成功:0
失败:-1
大多数的socket可选项是int类型,少数的需要特定的结构体类型。
//结构体timeval声明见《05—IO模型与多路复用》
示例:使用UDP协议,在服务器端设定超时检测。若在一定时间内客户端未发送数据则报错退出
//由于TCP协议的可靠性,因此当网络无法连接时recv()/send()会自动报错,因此使用UDP协议演示网络连接超时
//服务器端
//文件server.c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#define N 64
int main(int argc, const char *argv[])
{
if(argc<3)
{
printf("too few arguments\n");
printf("Usage: %s <ip> <port>\n",argv[0]);
exit(0);
}
int sockfd;
char buf[N];
struct sockaddr_in servaddr;
struct timeval time = {6,0};//设置超时时间为6s
//结构体timeval声明见《05—IO模型与多路复用》
if((sockfd = socket(AF_INET,SOCK_DGRAM,0))<0)
{
perror("socket");
exit(0);
}
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(atoi(argv[2]));
servaddr.sin_addr.s_addr = inet_addr(argv[1]);
if(bind(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
{
perror("bind");
exit(0);
}
printf("bind success\n");
//在接收客户端信息之间设定socket()选项,设定超时时间
if(setsockopt(sockfd,SOL_SOCKET,SO_RCVTIMEO,&time,sizeof(time))<0)
{
perror("cannot set socket");
exit(0);
}
if(recvfrom(sockfd,buf,N,0,NULL,NULL)<0)
{
perror("recvfrom");
exit(0);
}
printf("recv data:%s",buf);
return 0;
}
该程序运行后,在6s内未收到客户端发来的信息,则会自动报错退出。
一、广播(Broadcast)
在之前我们学习的网络编程中,服务器端与客户端都采用“一对一”的通信形式进行通信,即通信有唯一的发送方与唯一的接收方,这种通信形式称为“单播”(Unicast)。
有些时候,我们需要将数据发送到该局域网内的所有主机,若采用单播的形式则十分麻烦,我们可以采用“广播”(Broadcast)的形式将数据发送到该局域网的所有主机。
1、广播地址
在Internet中,使用IP地址标识网络中的每一台主机。其中,每个网段都对应一个广播地址,广播IP地址通常为该网段的最大IP地址。
例如,在192.168.1.xxx网段中,最大IP地址为192.168.1.255,则该IP地址对应了该网段的广播地址。
当我们向广播IP地址发送数据时,该网段的所有主机都会接收到该数据。
//注意:广播和组播仅应用于UDP协议,TCP协议是面向连接的,因此不支持广播和组播。
2、广播的发送与接收
广播的发送与接收通过UDP协议实现。
发送广播包的流程如下:
1.创建UDP套接字
2.绑定IP地址与端口(通常为广播IP地址)
3.设置套接字选项,设定套接字允许发送广播包
4.发送广播包
而接收广播包的流程与UDP客户端通常的编程流程类似:
1.创建UDP套接字
2.绑定IP地址与端口(通常为广播IP地址)
3.接收数据包
注意:通常情况下,接收广播的接收端需要绑定发送广播的IP地址与端口,否则可能无法接收广播
示例:广播发送端向广播地址192.168.1.255发送信息,发送信息由键盘输入。当输入"byebye"时停止广播
//广播发送端
//文件broadcast_send.c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#define N 64
int main(int argc, const char *argv[])
{
if(argc<3)
{
printf("too few arguments\n");
printf("Usage: %s <ip> <port>\n",argv[0]);
exit(0);
}
int sockfd;
int on=1;
char buf[N];
struct sockaddr_in servaddr;
if((sockfd = socket(AF_INET,SOCK_DGRAM,0))<0)
{
perror("socket");
exit(0);
}
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(atoi(argv[2]));
servaddr.sin_addr.s_addr = inet_addr(argv[1]);
//套接字默认不允许发送广播,需要使用setsockopt()设定开启发送广播使能
if(setsockopt(sockfd,SOL_SOCKET,SO_BROADCAST,&on,sizeof(on))<0)//设定发送广播使能
{
perror("cannot set socket");
exit(0);
}
//若广播发送端与接收端未使用同一端口,则广播接收端可能无法接收广播消息
/*if(setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))<0)//设定端口允许复用
{
perror("cannot reuse");
exit(0);
}*/
while(1)
{
printf("输入发送的广播信息:");
fgets(buf,N,stdin);
if(sendto(sockfd,buf,sizeof(buf),0,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
{
perror("send message");
}
else
{
printf("send message success\n");
}
if(strncmp(buf,"byebye",6)==0)
{
printf("Disconnect\n");
break;
}
}
close(sockfd);
return 0;
}
//广播接收端
//文件broadcast_recv
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#define N 64
int main(int argc, const char *argv[])
{
if(argc<3)
{
printf("too few arguments\n");
printf("Usage: %s <ip> <port>\n",argv[0]);
exit(0);
}
int sockfd;
int on=1;
char buf[N];
struct sockaddr_in myaddr,peeraddr;
socklen_t peerlen;
if((sockfd = socket(AF_INET,SOCK_DGRAM,0))<0)
{
perror("socket");
exit(0);
}
/*if(setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))<0)//设定端口允许复用
{
perror("cannot set socket");
exit(0);
}*/
bzero(&myaddr,sizeof(myaddr));
myaddr.sin_family = AF_INET;
myaddr.sin_port = htons(atoi(argv[2]));
myaddr.sin_addr.s_addr = inet_addr(argv[1]);
if(bind(sockfd,(struct sockaddr *)&myaddr,sizeof(myaddr))<0)//需要绑定广播端口,否则可能无法接收广播消息
{
perror("bind");
exit(0);
}
printf("bind success\n");
peerlen = sizeof(peeraddr);
while(1)
{
if(recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr*)&peeraddr,&peerlen)<0)
{
perror("recvfrom");
exit(0);
}
printf("[%s:%d] %s",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port),buf);
if(strncmp(buf,"byebye",6)==0)
{
printf("Disconnect\n");
break;
}
}
close(sockfd);
return 0;
}
二、组播(多播,Multicast)
使用广播可以十分方便地向局域网内所有主机发送数据包,但频繁使用广播会造成局域网内的数据风暴,造成网络拥堵。而且对于不想接收广播数据包的主机来说,广播并非是必要的(甚至是有害的,因为可能造成网络拥堵)。
我们可以使用组播代替广播。组播(也称为“多播”)可以视为广播与单播的折中方案。当发送组播数据时,只有加入了组播组的主机才会接收到数据并处理,其他未加入组播组的主机会直接丢弃接收到的组播数据包。也就是说,我们可以通过组播的方式指定特定的若干主机进行通信。
1、组播地址
在IPv4中,D类IP地址(224.0.0.0~239.255.255.255)用于组播地址,每一个组播IP地址代表了一个组播组,若想使用组播通信,需要加入对应的组播组。
2、组播的发送与接收
若想加入一个组播组接收组播信息,则需要通过setsockopt()设定套接字选项。加入组播组需要设定struct ip_mreq结构体作为参数
结构体struct ip_mreq声明如下:
struct ip_mreq
{
imr_multiaddr //需要加入的组播IP
imr_interface //本地IP,与组播组IP连接的本地接口,通常设定为INADDR_ANY
};
发送组播包的流程如下:
1.创建UDP套接字
2.绑定目标地址和端口(某个组播IP地址,可选)
3.发送数据包
可以发现,组播包发送端与广播包发送端基本类似,只是无需设置socket为广播
接收组播包流程如下:
1.创建UDP套接字
2.加入组播组
3.绑定目标地址和端口(某个组播IP地址)
4.接收组播包
5.离开组播组
若加入/离开某个组播组,则需要使用setsockopt()设定socket()加入/离开组播。
示例:组播发送端向组播地址224.10.10.1发送信息,发送信息由键盘输入。当输入"byebye"时停止组播
//组播发送端
//文件mul_send.c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#define N 64
int main(int argc, const char *argv[])
{
if(argc<3)
{
printf("too few arguments\n");
printf("Usage: %s <ip> <port>\n",argv[0]);
exit(0);
}
int sockfd;
int on=1;
char buf[N];
struct sockaddr_in servaddr;
if((sockfd = socket(AF_INET,SOCK_DGRAM,0))<0)
{
perror("socket");
exit(0);
}
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(atoi(argv[2]));
servaddr.sin_addr.s_addr = inet_addr(argv[1]);
while(1)
{
printf("输入发送的组播信息:");
fgets(buf,N,stdin);
if(sendto(sockfd,buf,sizeof(buf),0,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
{
perror("send message");
}
if(strncmp(buf,"byebye",6)==0)
{
printf("Disconnect\n");
break;
}
}
close(sockfd);
return 0;
}
//组播接收端
//文件mul_recv.c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#define N 64
int main(int argc, const char *argv[])
{
if(argc<3)
{
printf("too few arguments\n");
printf("Usage: %s <ip> <port>\n",argv[0]);
exit(0);
}
int sockfd;
int on=1;
char buf[N];
struct ip_mreq mreq;
struct sockaddr_in myaddr,peeraddr;
socklen_t peerlen = sizeof(peeraddr);
if((sockfd = socket(AF_INET,SOCK_DGRAM,0))<0)
{
perror("socket");
exit(0);
}
bzero(&mreq,sizeof(mreq));
mreq.imr_multiaddr.s_addr = inet_addr(argv[1]);
mreq.imr_interface.s_addr = htonl(INADDR_ANY);
//imr_interface成员通常设定为INADDR_ANY
//INADDR_ANY意思为“所有地址”或“任意地址”,其值为0.0.0.0,表示一个不确定的IP地址
//将第二个地址设定为INADDR_ANY,意思为让路由器根据当前路由表自动选择本地接口作为组播接口
//若不设置为该值,则可能会查找路由表失败从而无法加入组播组
if(setsockopt(sockfd,IPPROTO_IP,IP_ADD_MEMBERSHIP,&mreq,sizeof(mreq))<0)//加入组播组
{
perror("cannot join multicast");
exit(0);
}
bzero(&myaddr,sizeof(myaddr));
myaddr.sin_family = AF_INET;
myaddr.sin_port = htons(atoi(argv[2]));
myaddr.sin_addr.s_addr = inet_addr(argv[1]);
if(bind(sockfd,(struct sockaddr*)&myaddr,sizeof(myaddr))<0)
{
perror("bind");
exit(0);
}
while(1)
{
if(recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr*)&peeraddr,&peerlen)<0)
{
perror("recvfrom");
exit(0);
}
printf("[%s:%d] %s",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port),buf);
if(strncmp(buf,"byebye",6)==0)
{
printf("Disconnect\n");
break;
}
}
if(setsockopt(sockfd,IPPROTO_IP,IP_DROP_MEMBERSHIP,&mreq,sizeof(mreq))<0)//离开组播组
{
perror("cannot exit multicast");
exit(0);
}
close(sockfd);
return 0;
}
}
//客户端代码同循环服务器