单播:前几篇博客介绍的数据包发送方式只有一个接受方,称为单播
广播:如果同时发给局域网中的所有主机,称为广播
组播:如果只发给局域网内的部分主机,称为组播
同一个sockfd 只能处理一种接收方式,如果即想发单播又想发广播,还想发组播,需要三个sockfd。只有用户数据报(使用UDP协议)套接字才能广播。
以192.168.1.0 (255.255.255.0) 网段为例,最大的主机地址192.168.1.255代表该网段的广播地址(具体以ifconfig 命令查看到的 broadcast 后面的为准)发到该地址的数据包被所有的主机接收。
注:255.255.255.255在所有网段中都代表广播地址。
广播能发给局域网所有主机的原理:
因为广播的数据包比较特殊,他的目的mac地址全是f(ff:ff:ff:ff:ff:ff) 这个数据包会发给交换机,交换机是工作在链路层的,交换机看到这样目的mac全是f的数据包,就会将该数据包发给局域网内的所有主机。到达主机后,进行拆包,看到目的mac是广播的mac,则允许通过。到达网络层一看IP地址是广播的IP地址,则可以通过。到达传输层,只要端口号匹配,则数据就能到达应用层。
广播的应用:ARP请求,通过ip地址获取对方的mac地址,使用的就是广播。
发送者:
创建套接字 socket( )
设置为允许发送广播权限 setsockopt( )
填充广播信息结构体 sockaddr_in
发送数据 sendto( )
接收者:
创建套接字 socket( )
填充广播信息结构体 sockaddr_in
将套接字与广播信息结构体绑定 bind( )
接收数据 recvfrom( )
getsockopt函数说明:
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
功能:设置允许发送广播
#include
#include
参数:
@sockfd: socket函数返回的套接字
@level:SOL_SOCKET 套接字级别
@optname:SO_BROADCAST 允许发送广播
@optval:int on = 1
@optlen:sizeof(on)
返回值:成功返回0,失败返回-1,置位错误码
示例:
int on = 1;
if(-1 == setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on))){
ERRLOG("setsockopt error");
}
//接收端
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define N 128
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s--%s(%d)\n", __FILE__, __func__, __LINE__);\
exit(-1);\
}while(0)
int main(int argc, const char *argv[]){
if(3 != argc){
printf("Usage : %s \n", argv[0]);
exit(-1);
}
//1.创建用户数据报式套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(-1 == sockfd){
ERRLOG("socket error");
}
//创建服务器广播信息结构体
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
//端口号随便定 ip地址需要是 广播的ip地址
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[2]));
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
socklen_t addrlen = sizeof(server_addr);
//3.将套接字和广播信息结构体进行绑定
if(-1 == bind(sockfd, (struct sockaddr *)&server_addr, addrlen)){
ERRLOG("bind error");
}
//定义一个结构体,保存客户端的信息
struct sockaddr_in client_addr;
memset(&server_addr, 0, sizeof(client_addr));//清空
socklen_t clientaddrlen = sizeof(client_addr);
char buff[N] = {0};
while(1){
//接收数据,如果想要给对方回应,就必须保存对方的网络信息结构体
//如果不回应,后两个参数写 NULL 也行
if(-1 == recvfrom(sockfd, buff, N, 0, (struct sockaddr *)&client_addr, &clientaddrlen)){
ERRLOG("recvfrom error");
}
printf("%s(%d):%s\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), buff);
#if 0
//组装应答信息
strcat(buff, "--sever");
if(-1 == sendto(sockfd, buff, N, 0, (struct sockaddr *)&client_addr, clientaddrlen)){
ERRLOG("sendto error");
}
#endif
memset(buff, 0, N);
}
//关闭套接字
close(sockfd);
return 0;
}
//发送端
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define N 128
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s--%s(%d)\n", __FILE__, __func__, __LINE__);\
exit(-1);\
}while(0)
int main(int argc, const char *argv[]){
if(3 != argc){
printf("Usage : %s \n", argv[0]);
exit(-1);
}
//1.创建用户数据报套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(-1 == sockfd){
ERRLOG("socket error");
}
//设置套接字允许发送广播 setsockopt
int on = 1;
if(-1 == setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on))){
ERRLOG("setsockopt error");
}
//填充服务器广播信息结构体
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[2]));
//广播的ip地址
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
socklen_t addrlen = sizeof(server_addr);
char buff[N] = {0};
while(1){
printf("input your msg:");
fgets(buff, N, stdin);
buff[strlen(buff)-1] = '\0';//清除 \n
if(0 == strcmp(buff, "quit")){
break;
}
if(-1 == sendto(sockfd, buff, N, 0, (struct sockaddr *)&server_addr, addrlen)){
ERRLOG("sendto error");
}
#if 0
//因为客户端已经知道服务器的网络信息了 所以后两个参数可以传NULL
if(-1 == recvfrom(sockfd, buff, N, 0, NULL, NULL)){
ERRLOG("recvfrom error");
}
printf("recv:[%s]\n", buff);
#endif
memset(buff, 0, N);
}
//关闭套接字
close(sockfd);
return 0;
}
单播方式只能发给一个接收方。广播方式发给所有的主机。过多的广播会大量占用网络带宽,造成广播风暴,影响正常的通信。组播(又称为多播)是一种折中的方式。只有加入某个多播组的主机才能收到数据。
使用SOCK_DGRAM多播方式既可以发给多个主机,又能避免像广播那样带来过多的负载(每台主机要到传输层才能判断广播包是否要处理)
D类地址(组播地址)
不分网络地址和主机地址,第1字节的前4位固定为1110(224.0.0.1 – 239.255.255.255)
发送者:
创建套接字 socket( )
填充组播信息结构体 sockaddr_in
发送数据 sendto( )
接收者:
创建套接字 scoket( )
填充组播信息结构体 sockaddr_in
将套接字与组播信息结构体绑定 bind( )
设置为加入多播组 setsockopt( )
接收数据 recvfrom( )
setsockopt函数说明:
int setsockopt(int sockfd, int level, int optname, void *optval, socklen_t optlen);
功能:设置套接字加入多播组
#include
#include
参数:
@sockfd: socket函数返回的套接字
@level:IPPROTO_IP IP级别
@optname:IP_ADD_MEMBERSHIP 选项名 设置加入多播组
@optval:
struct ip_mreqn {
struct in_addr imr_multiaddr; /* 多播组的地址 */
struct in_addr imr_address; /* 本地地址 */
int imr_ifindex; /* 接口索引 我们基本不用 设置0即可 */
};
//或者使用不带n的结构体
struct ip_mreq {
struct in_addr imr_multiaddr; /* 多播组的地址 */
struct in_addr imr_address; /* 本地地址 */
};
@optlen:optval的大小
返回值:成功返回0,失败返回-1,置位错误码
//接收端
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define N 128
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s--%s(%d)\n", __FILE__, __func__, __LINE__);\
exit(-1);\
}while(0)
int main(int argc, const char *argv[]){
if(3 != argc){
printf("Usage : %s \n", argv[0]);
exit(-1);
}
//1.创建用户数据报式套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(-1 == sockfd){
ERRLOG("socket error");
}
//创建服务器网络信息结构体
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[2]));
//多播组的ip地址 224.0.0.1 – 239.255.255.255
server_addr.sin_addr.s_addr = inet_addr("224.0.0.1");
socklen_t addrlen = sizeof(server_addr);
//3.将套接字和广播信息结构体进行绑定
if(-1 == bind(sockfd, (struct sockaddr *)&server_addr, addrlen)){
ERRLOG("bind error");
}
//设置加入多播组
struct ip_mreq my_mreq;
memset(&my_mreq, 0, sizeof(my_mreq));
my_mreq.imr_multiaddr.s_addr = inet_addr("224.0.0.1");
my_mreq.imr_interface.s_addr = inet_addr(argv[1]);
if(-1 == setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &my_mreq, sizeof(my_mreq))){
ERRLOG("setsockopt error");
}
//定义一个结构体,保存客户端的信息
struct sockaddr_in client_addr;
memset(&server_addr, 0, sizeof(client_addr));//清空
socklen_t clientaddrlen = sizeof(client_addr);
char buff[N] = {0};
while(1){
//接收数据,如果想要给对方回应,就必须保存对方的网络信息结构体
//如果不回应,后两个参数写 NULL 也行
if(-1 == recvfrom(sockfd, buff, N, 0, (struct sockaddr *)&client_addr, &clientaddrlen)){
ERRLOG("recvfrom error");
}
printf("%s(%d):%s\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), buff);
#if 0
//组装应答信息
strcat(buff, "--sever");
if(-1 == sendto(sockfd, buff, N, 0, (struct sockaddr *)&client_addr, clientaddrlen)){
ERRLOG("sendto error");
}
#endif
memset(buff, 0, N);
}
//关闭套接字
close(sockfd);
return 0;
}
//发送端
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define N 128
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s--%s(%d)\n", __FILE__, __func__, __LINE__);\
exit(-1);\
}while(0)
int main(int argc, const char *argv[]){
if(3 != argc){
printf("Usage : %s \n", argv[0]);
exit(-1);
}
//1.创建用户数据报套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(-1 == sockfd){
ERRLOG("socket error");
}
//填充服务器组播信息结构体
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[2]));
//多播组的ip地址 224.0.0.1 – 239.255.255.255
//这个范围内的每个ip地址 都表示一个多播组
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
socklen_t addrlen = sizeof(server_addr);
char buff[N] = {0};
while(1){
printf("input your msg:");
fgets(buff, N, stdin);
buff[strlen(buff)-1] = '\0';//清除 \n
if(0 == strcmp(buff, "quit")){
break;
}
if(-1 == sendto(sockfd, buff, N, 0, (struct sockaddr *)&server_addr, addrlen)){
ERRLOG("sendto error");
}
memset(buff, 0, N);
}
//关闭套接字
close(sockfd);
return 0;
}
注:如果接收不到组播消息:
先执行一下命令: sudo ifconfig 网卡名 allmulti
网卡名可以使用 ifconfig命令查出来
socket最早起也是用于本地通信的,后来有了TCP/IP协议族的的加入,才可以实现不同主机的进程间通信。本地通信和其他进程间通信方式相比使用方便、效率更高,多用于前后台进程通信。
创建套接字时使用本地协议AF_UNIX(或AF_LOCAL)。分为流式套接字和用户数据报套接字 ----区别还是有无连接。
struct sockaddr_un {
sa_family_t sun_family; /* AF_UNIX */
char sun_path[108]; /* 带路径的文件名 */
};
服务器:
创建套接字 socket( )
填充服务器本地信息结构体 sockaddr_un
将套接字与服务器本地信息结构体绑定 bind( )
将套接字设置为被动监听状态 listen( )
阻塞等待客户端的连接请求 accept( )
进行通信 recv( )/send( ) 或 read( )/write( )
客户端:
创建套接字 socket( )
填充服务器本地信息结构体 sockaddr_un
发送客户端的连接请求 connect( )
进行通信 send( )/recv( )
代码实现:
//服务器端
#include
#include
#include
#include
#include
#include
#include
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s-%s(%d)\n", __FILE__, __func__, __LINE__);\
exit(-1);\
}while(0)
int main(int argc, char *argv[]){
if(2!=argc){
printf("Usage : %s \n", argv[0]);
exit(-1);
}
//1.创建套接字
int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
if(-1 == sockfd){
ERRLOG("socket error");
}
//2.填充服务器本地信息结构体
struct sockaddr_un server_addr;
memset(&server_addr, 0, sizeof(server_addr));//清空
server_addr.sun_family = AF_UNIX;
//注意:sun_path是一个数组 不能使用 等号赋值
strcpy(server_addr.sun_path, argv[1]);
socklen_t addrlen = sizeof(server_addr);
//3.将套接字和网络信息结构体进行绑定
if(-1 == bind(sockfd, (struct sockaddr *)&server_addr, addrlen)){
ERRLOG("bind error");
}
//4.将服务器的套接字设置成被动监听状态
if(-1 == listen(sockfd, 5)){
ERRLOG("listen error");
}
//定义一个结构体,保存客户端的信息
struct sockaddr_un client_addr;
memset(&client_addr, 0, sizeof(client_addr));//清空
socklen_t clientaddrlen = sizeof(client_addr);
char buff[128] = {0};
int acceptfd = 0;
int bytes = 0;
while(1){
//5.阻塞等待客户端连接
acceptfd = accept(sockfd, (struct sockaddr *)&client_addr, &clientaddrlen);
if(-1 == acceptfd){
ERRLOG("accept error");
}
printf("客户端 [%s] 连接到服务器了\n", client_addr.sun_path);
while(1){
//6.与客户端通信
if(0 > (bytes = recv(acceptfd, buff, 128, 0))){
ERRLOG("recv error");
}else if(bytes == 0){
printf("客户端 [%s] 断开了连接\n", client_addr.sun_path);
break;
}else{
if(0 == strcmp(buff, "quit")){
printf("客户端 [%s] 退出了\n", client_addr.sun_path);
break;
}
printf("客户端 [%s] 发来数据:[%s]\n", client_addr.sun_path, buff);
//组装应答
strcat(buff, "--sever");
if(-1 == send(acceptfd, buff, 128, 0)){
ERRLOG("send error");
}
}
}
//7.关闭套接字
close(acceptfd);
}
close(sockfd);
return 0;
}
//客户端
#include
#include
#include
#include
#include
#include
#include
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s-%s(%d)\n", __FILE__, __func__, __LINE__);\
exit(-1);\
}while(0)
int main(int argc, char *argv[]){
if(2!=argc){
printf("Usage : %s \n", argv[0]);
exit(-1);
}
//1.创建套接字
int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
if(-1 == sockfd){
ERRLOG("socket error");
}
struct sockaddr_un server_addr;
memset(&server_addr, 0, sizeof(server_addr));//清空
server_addr.sun_family = AF_UNIX;
//注意:sun_path是一个数组 不能使用 等号赋值
strcpy(server_addr.sun_path, argv[1]);
socklen_t addrlen = sizeof(server_addr);
//定义一个结构体,保存客户端的信息
struct sockaddr_un client_addr;
memset(&client_addr, 0, sizeof(client_addr));//清空
client_addr.sun_family = AF_UNIX;
strcpy(client_addr.sun_path,"tcp_client_sock_file");
socklen_t clientaddrlen = sizeof(client_addr);
if(-1 == bind(sockfd, (struct sockaddr *)&client_addr, clientaddrlen)){
ERRLOG("bind error");
}
//3.与服务器建立连接
if(-1 == connect(sockfd, (struct sockaddr *)&server_addr, addrlen)){
ERRLOG("connect error");
}
//4.与服务器通信
char buff[128] = {0};
while(1){
fgets(buff, 128, stdin);
buff[strlen(buff)-1] = '\0';//清除 \n
if(-1 == send(sockfd, buff, 128, 0)){
ERRLOG("send error");
}
if(0 == strcmp(buff, "quit")){
break;
}
if(-1 == recv(sockfd, buff, 128, 0)){
ERRLOG("recv error");
}
printf("收到回复:[%s]\n", buff);
}
//5.关闭套接字
close(sockfd);
return 0;
}
服务器:
创建套接字 socket( ) SOCK_DGRAM
填充服务器本地信息结构体 sockaddr_un
将套接字与服务器本地信息结构体绑定 bind( )
进行通信 recvfrom( )/sendto( )
客户端:
创建套接字 socket( )
填充客户端本地信息结构体 sockaddr_un
将套接字与客户端本地信息结构体绑定 bind( ) //要绑定 否则服务器无法给客户端回信
填充服务器本地信息结构体 sockaddr_un
进行通信 sendto( ) / recvfrom( )
代码实现:
//服务器端
#include
#include
#include
#include
#include
#include
#include
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s-%s(%d)\n", __FILE__, __func__, __LINE__);\
exit(-1);\
}while(0)
int main(int argc, char *argv[]){
if(2!=argc){
printf("Usage : %s \n", argv[0]);
exit(-1);
}
//1.创建套接字
int sockfd = socket(AF_UNIX, SOCK_DGRAM, 0);
if(-1 == sockfd){
ERRLOG("socket error");
}
//2.填充服务器本地信息结构体
struct sockaddr_un server_addr;
memset(&server_addr, 0, sizeof(server_addr));//清空
server_addr.sun_family = AF_UNIX;
//注意:sun_path是一个数组 不能使用 等号赋值
strcpy(server_addr.sun_path, argv[1]);
socklen_t addrlen = sizeof(server_addr);
//3.将套接字和网络信息结构体进行绑定
if(-1 == bind(sockfd, (struct sockaddr *)&server_addr, addrlen)){
ERRLOG("bind error");
}
//定义一个结构体,保存客户端的信息
//给发送者回信 需要用到
struct sockaddr_un client_addr;
memset(&client_addr, 0, sizeof(client_addr));//清空
socklen_t clientaddrlen = sizeof(client_addr);
char buff[128] = {0};
int bytes = 0;
while(1){
//6.与客户端通信
if(0 > (bytes = recvfrom(sockfd, buff, 128, 0, (struct sockaddr *)&client_addr, &clientaddrlen))){
ERRLOG("recvfrom error");
}else{
printf("客户端 [%s] 发来数据:[%s]\n", client_addr.sun_path, buff);
//组装应答
strcat(buff, "--sever");
if(-1 == sendto(sockfd, buff, 128, 0, (struct sockaddr *)&client_addr, clientaddrlen)){
ERRLOG("sendto error");
}
}
}
close(sockfd);
return 0;
}
//客户端
#include
#include
#include
#include
#include
#include
#include
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s-%s(%d)\n", __FILE__, __func__, __LINE__);\
exit(-1);\
}while(0)
int main(int argc, char *argv[]){
if(3!=argc){
printf("Usage : %s \n", argv[0]);
exit(-1);
}
//1.创建套接字
int sockfd = socket(AF_UNIX, SOCK_DGRAM, 0);
if(-1 == sockfd){
ERRLOG("socket error");
}
//服务器的本地信息结构体
struct sockaddr_un server_addr;
memset(&server_addr, 0, sizeof(server_addr));//清空
server_addr.sun_family = AF_UNIX;
//注意:sun_path是一个数组 不能使用 等号赋值
strcpy(server_addr.sun_path, argv[1]);
socklen_t addrlen = sizeof(server_addr);
//定义一个结构体,保存自己的信息
struct sockaddr_un client_addr;
memset(&client_addr, 0, sizeof(client_addr));//清空
client_addr.sun_family = AF_UNIX;
strcpy(client_addr.sun_path, argv[2]);
socklen_t clientaddrlen = sizeof(client_addr);
if(-1 == bind(sockfd, (struct sockaddr *)&client_addr, clientaddrlen)){
ERRLOG("bind error");
}
//4.与服务器通信
char buff[128] = {0};
while(1){
fgets(buff, 128, stdin);
buff[strlen(buff)-1] = '\0';//清除 \n
if(-1 == sendto(sockfd, buff, 128, 0, (struct sockaddr *)&server_addr, addrlen)){
ERRLOG("sendto error");
}
//无需再保存服务器的 本地信息结构体了 因为server_addr我们没修改过
if(-1 == recvfrom(sockfd, buff, 128, 0, NULL, NULL)){
ERRLOG("recvfrom error");
}
printf("收到回复:[%s]\n", buff);
}
//5.关闭套接字
close(sockfd);
return 0;
}
至此,“网络编程” 专栏的全部知识讲完了,下面的小项目检测一下学习成果,建议先试着自己写,不会的部分再看下面的代码。
注:接下来会更新Python专栏,喜欢的我的博客可以点赞关注,持续更新中。
①有新用户登录,其他在线的用户可以收到登录信息
②有用户群聊,其他在线的用户可以收到群聊信息
③有用户退出,其他在线的用户可以收到退出信息
④服务器可以发送系统信息
提示:
①客户端登录之后,为了实现一边发送数据一边接收数据,可以使用多进程或者多线程
②服务器既可以发送系统信息,又可以接收客户端信息并处理,可以使用多进程或者多线程
③服务器需要给多个用户发送数据,所以需要保存每一个用户的信息,使用链表来保存
④数据传输的时候要定义结构体,结构体中包含操作码、用户名以及数据
①画流程图
②根据流程图写框架
③一个功能一个功能实现
流程图:
//服务器端
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s-%s(%d)\n", __FILE__, __func__, __LINE__);\
exit(-1);\
}while(0)
typedef struct __MSG{
char code;
char name[32];
char text[128];
}msg_t;
typedef struct __NODE{
struct sockaddr_in clientaddr;
struct __NODE *next;
}node_t;
void do_login(int sockfd, node_t * phead, msg_t msg, struct sockaddr_in clientaddr);
void do_chat(int sockfd, node_t * phead, msg_t msg, struct sockaddr_in clientaddr);
void do_quit(int sockfd, node_t * phead, msg_t msg, struct sockaddr_in clientaddr);
void create_node(node_t **pnew);
int main(int argc, char *argv[]){
//对入参合理性进行检查
if(3 != argc){
printf("Usage : %s \n", argv[0]);
exit(-1);
}
//创建数据报式套接字
int sockfd = 0;
if(-1 == (sockfd = socket(AF_INET, SOCK_DGRAM, 0))){
ERRLOG("socket error");
}
//填充服务器的网络信息结构体
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(atoi(argv[2]));
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
socklen_t serveraddr_len = sizeof(serveraddr);
//绑定套接字与服务器网络信息结构体
if(-1 == bind(sockfd, (struct sockaddr *)&serveraddr, serveraddr_len)){
ERRLOG("bind error");
}
//创建子进程
pid_t pid = 0;
if(-1 == (pid = fork())){
ERRLOG("fork error");
}else if(0 == pid){//子进程逻辑
//创建保存客户端网络信息结构体的链表的头结点
node_t *phead = NULL;
create_node(&phead);
//定义结构体保存客户端的信息
struct sockaddr_in clientaddr;
memset(&clientaddr, 0, sizeof(clientaddr));
socklen_t clientaddr_len = sizeof(clientaddr);
msg_t msg;
while(1){
memset(&msg, 0, sizeof(msg));
if(-1 == recvfrom(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&clientaddr, &clientaddr_len)){
ERRLOG("recvfrom error");
}
//根据操作码不同,做不同的操作
switch(msg.code){
case 'L':
do_login(sockfd, phead, msg, clientaddr);
break;
case 'C':
do_chat(sockfd, phead, msg, clientaddr);
break;
case 'Q':
do_quit(sockfd, phead, msg, clientaddr);
break;
}
}
close(sockfd);
}else if(0 < pid){
//父进程逻辑
//发送系统消息:把父进程当做客户端给子进程发送群聊消息
msg_t msg;
while(1){
memset(&msg, 0, sizeof(msg));
msg.code = 'C';
strcpy(msg.name, "server");
fgets(msg.text, 128, stdin);
msg.text[strlen(msg.text)-1] = '\0';
if(-1 == sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&serveraddr, serveraddr_len)){
ERRLOG("sendto error");
}
}
close(sockfd);
}
return 0;
}
void create_node(node_t **pnew){
*pnew = (node_t *)malloc(sizeof(node_t));
memset((*pnew), 0, sizeof(node_t));
}
//登录操作
void do_login(int sockfd, node_t * phead, msg_t msg, struct sockaddr_in clientaddr){
//将xx加入群聊的消息发给链表的所有节点
node_t *ptemp = phead;
while(ptemp->next != NULL){
ptemp = ptemp->next;
if(-1 == sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&(ptemp->clientaddr), sizeof(ptemp->clientaddr))){
ERRLOG("sendto error");
}
}
//将新客户端的网络信息结构体插入链表--头插法
node_t *pnew = NULL;
create_node(&pnew);
pnew->clientaddr = clientaddr;
pnew->next = phead->next;
phead->next = pnew;
}
//群聊操作
void do_chat(int sockfd, node_t * phead, msg_t msg, struct sockaddr_in clientaddr){
node_t *ptemp = phead;
while(ptemp->next != NULL){
ptemp = ptemp->next;
if(memcmp(&(ptemp->clientaddr), &clientaddr, sizeof(clientaddr))){
//进到这里说明 不是自己
if(-1 == sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&(ptemp->clientaddr), sizeof(ptemp->clientaddr))){
ERRLOG("sendto error");
}
}
}
}
//退出操作
void do_quit(int sockfd, node_t * phead, msg_t msg, struct sockaddr_in clientaddr){
//先将要突出的客户端网络信息结构体在链表中删除
node_t *ptemp = phead;
node_t *pdel = NULL;
while(ptemp->next != NULL){
if(!memcmp(&(ptemp->next->clientaddr), &clientaddr, sizeof(clientaddr))){
//说明是自己
pdel = ptemp->next;
ptemp->next = pdel->next;
free(pdel);
pdel = NULL;
}else{
if(-1 == sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&(ptemp->next->clientaddr), sizeof(ptemp->next->clientaddr))){
ERRLOG("sendto error");
}
ptemp = ptemp->next;
}
}
}
//客户端
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s-%s(%d)\n", __FILE__, __func__, __LINE__);\
exit(-1);\
}while(0)
typedef struct __MSG{
char code;
char name[32];
char text[128];
}msg_t;
int main(int argc, char *argv[]){
//对入参合理性进行检查
if(3 != argc){
printf("Usage : %s \n", argv[0]);
exit(-1);
}
//创建数据报式套接字
int sockfd = 0;
if(-1 == (sockfd = socket(AF_INET, SOCK_DGRAM, 0))){
ERRLOG("socket error");
}
//填充服务器的网络信息结构体
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(atoi(argv[2]));
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
socklen_t serveraddr_len = sizeof(serveraddr);
//先发送登录消息
msg_t msg;
memset(&msg, 0, sizeof(msg));
msg.code = 'L';
printf("请输入用户名:");
fgets(msg.name, 32, stdin);
msg.name[strlen(msg.name)-1] = '\0';
strcpy(msg.text, "加入了群聊");
if(-1 == sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&serveraddr, serveraddr_len)){
ERRLOG("sendto error");
}
//创建子进程
pid_t pid = 0;
if(-1 == (pid = fork())){
ERRLOG("fork error");
}else if(0 == pid){//子进程
while(1){
memset(&msg, 0, sizeof(msg));
if(-1 == recvfrom(sockfd, &msg, sizeof(msg), 0, NULL, NULL)){
ERRLOG("recvfrom error");
}
printf("%s : %s\n", msg.name, msg.text);
}
}else if(0 < pid){//父进程
while(1){
memset(msg.text, 0, sizeof(msg.text));
fgets(msg.text, 128, stdin);
msg.text[strlen(msg.text)-1] = '\0';
if(!strcmp(msg.text, "quit")){
msg.code = 'Q';
strcpy(msg.text,"退出群聊");
}else{
msg.code = 'C';
}
if(-1 == sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&serveraddr, serveraddr_len)){
ERRLOG("sendto error");
}
if(!strcmp(msg.text, "退出群聊")){
break;
}
}
//让子进程退出
kill(pid, SIGKILL);
wait(NULL);
//关闭套接字
close(sockfd);
}
return 0;
}