代码:
服务端代码:
#include <stdio.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#define N 128
#define L 1
#define C 2
#define Q 3
typedef struct{
int type;
char name[N];
char text[N];
}MSG; // 存信息
typedef struct node{
struct sockaddr_in addr; // 存ip 和 端口号
struct node *next; // 链表
}linklist_t;
linklist_t *linklist_create(); // 创建链表函数
void do_login(MSG msg, linklist_t *h, int sockfd, struct sockaddr_in clientaddr); // 某客端上线,将数据发送给其他在线客户端
void do_chat(MSG msg, linklist_t *h, int sockfd, struct sockaddr_in clientaddr); // 将用户想要发送的数据广播给其他用户
void do_quit(MSG msg, linklist_t *h, int sockfd, struct sockaddr_in clientaddr); // 在链表中删除自己的记录,并将自己退出的信息发送給其他客户端
int main(int argc, const char *argv[]){
int sockfd;
struct sockaddr_in serveraddr, clientaddr; // 储存信息
socklen_t addrlen = sizeof(serveraddr);
if(argc < 3){
printf("argc number error\n");
return -1;
}
/* 创建套接字 */
if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0){ // IPV4、UDP协议、协议标志
printf("socket error\n");
return -1;
}
/* 填充服务器网络信息 */
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));
if(bind(sockfd, (struct sockaddr *)&serveraddr, addrlen) < 0){ // 套接字与服务器网络信息绑定、(套接字是中转站,bind将ip和端口信息存入中转站)
printf("bind error\n");
return -1;
}
MSG msg;
pid_t pid;
if((pid = fork()) < 0){
printf("fork error\n");
return -1;
}
else if(pid == 0){ // 子进程
msg.type = C;
strcpy(msg.name, "server");
while(1){
fgets(msg.text, N, stdin); // 等待控制台输入
msg.text[strlen(msg.text) - 1] = '\0';
sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr*)&serveraddr, addrlen); // 子进程和父进程绑定在同一个ip地址 和 端口号 子进程能向父进程发送给对方
}
}
else{ // 父进程负责接收数据并处理
linklist_t *h = linklist_create();
while(1){
recvfrom(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&clientaddr, &addrlen); // 从中转站接收数据直到有数据为止
printf("%d -- %s -- %s\n", msg.type, msg.name, msg.text); // 打印接收的数据
switch(msg.type){ // 根据数据的类型做不同操作
case L:
do_login(msg, h, sockfd, clientaddr); // 登录广播提醒
break;
case C:
do_chat(msg, h, sockfd, clientaddr); // 广播聊天
break;
case Q:
do_quit(msg, h, sockfd, clientaddr); // 广播退出
break;
}
}
}
return 0;
}
linklist_t *linklist_create(){ // 创建链表
linklist_t *h = (linklist_t *)malloc(sizeof(linklist_t)); // 创建一个链表节点,h为链表头部
h->next = NULL; // 整个链表只有一个节点
return h;
}
void do_login(MSG msg, linklist_t *h, int sockfd, struct sockaddr_in clientaddr){
linklist_t *p = h;
/* 用户登录信息发送给其他客户 */
sprintf(msg.text, "-------- %s login -------------", msg.name);
while(p->next != NULL){
sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&p->next->addr, sizeof(struct sockaddr_in)); // 发送那个给链表中所有对象
p = p->next;
}
linklist_t *temp = (linklist_t *) malloc(sizeof(linklist_t)); // 创建一个新结点
temp->addr = clientaddr; // 客户端信息存入结点
temp->next = h->next;
h->next = temp; // 将temp存入链表末尾
return;
}
void do_chat(MSG msg, linklist_t *h, int sockfd, struct sockaddr_in clientaddr){
char buf[N] = {};
linklist_t *p = h;
/* 将用户信息发送给其他在线的用户 */
sprintf(buf, "%s : %s", msg.name, msg.text);
strcpy(msg.text, buf); // 将数据存入msg
while(p->next != NULL){ // 发送数据
if(memcmp(&clientaddr, &p->next->addr, sizeof(clientaddr)) == 0){ // 数据是自己的就不传输了
p = p->next;
}
else{ // 其他人就发送
sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&p->next->addr, sizeof(struct sockaddr_in)); // 发送的数据、长度、发送的位置
p = p->next;
}
}
return;
}
void do_quit(MSG msg, linklist_t *h, int sockfd, struct sockaddr_in clientaddr){
linklist_t *p = h;
linklist_t *temp;
/* 将用户退出的信息发送给其他用户,并将其信息从链表中删除 */
sprintf(msg.text, "-------- %s offline --------", msg.name);
while(p->next != NULL){
if(memcmp(&clientaddr, &p->next->addr, sizeof(clientaddr)) == 0){ // 自己的就不发送
temp = p->next;
p->next = temp->next;
free(temp); // 释放本结点
temp = NULL; // 指针至为空指针
}
else{
sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&p->next->addr, sizeof(struct sockaddr_in)); // 不是自己就发送
p = p->next;
}
}
return;
}
客户端代码:
#include <stdio.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <strings.h>
#define N 128
#define L 1
#define C 2
#define Q 3
typedef struct{
int type;
char name[N];
char text[N];
}MSG; // 存信息
int main(int argc, const char *argv[]){
int sockfd;
struct sockaddr_in serveraddr, clientaddr; // 储存信息
socklen_t addrlen = sizeof(serveraddr);
if(argc < 3){
printf("argc number error\n");
return -1;
}
/* 创建套接字 */
if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0){ // IPV4、UDP协议、协议标志
printf("socket error\n");
return -1;
}
/* 填充服务器网络信息 */
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));
MSG msg;
msg.type = L;
printf("please enter your name: ");
fgets(msg.name, N, stdin); // 将控制台输入的信息传入name中
msg.name[strlen(msg.name) - 1] = '\0';
sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&serveraddr, addrlen); // 将数据发送给服务端
pid_t pid;
if((pid = fork()) < 0){
printf("fork error\n");
return -1;
}
else if(pid == 0){ // 子进程,发送数据
while(1){
fgets(msg.text, N, stdin); // 等待控制台输入
msg.text[strlen(msg.text) - 1] = '\0';
if(strncmp(msg.text, "quit", 4) == 0){ // 若要退出
msg.type = Q; // 退出广播
sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr*)&serveraddr, addrlen); // 子进程和父进程绑定在同一个ip地址 和 端口号 子进程能向父进程发送给对方
close(sockfd);
kill(getppid(), SIGKILL); // 退出父进程
return 0;
}
msg.type = C; // 聊天
sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr*)&serveraddr, addrlen); // 子进程和父进程绑定在同一个ip地址 和 端口号 子进程能向父进程发送给对方
}
}
else{ // 父进程负责接收数据
while(1){
recvfrom(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&serveraddr, &addrlen); // 从中转站接收数据直到有数据为止
printf("%s\n", msg.text); // 打印接收的数据
}
}
return 0;
}
效果:
总体效果是客户上线状态、退出状态、发送的消息都能通过广播,将信息发送给所有在线客户端,服务端能接收并显示所有客户端发送的消息,且也具备广播能力
原理:
服务端:
服务端创建了一个链表,这个链表中的每个节点是专用于储存客户端的ip地址和端口号等一系列信息,目的是方便遍历实现广播功能
服务端的创建了父子进程,子进程专门接收控制台发送的数据,并转发给父进程,父进程将数据广播给所有客户端,朴素的讲子进程接收控制台数据,父进程接收客户端信息并广播
客户端:
客户端也是创建父子进程,父进程负责接收服务器转发的数据,并打印。子进程负责发送,从本控制台获取的信息并发送给服务端通过服务器广播给其他客户端
服务器本质就是中转站,负责接收客户端信息状态并广播
拓展:
套接字:
套接字可以理解为网络通信的中转站,将通信双方的ip地址和端口号等相关信息存入套接字,以便通信双方能通过ip地址和端口号找到对应接收端。
服务端和客户端都创建套接字的原因:
在一个典型的客户端-服务器模型中,服务器和客户端通过套接字建立通信。一般情况下,服务器会先创建一个套接字并绑定到一个特定的 IP 地址和端口上,然后等待客户端连接。
在客户端与服务器建立连接时,客户端会创建一个新的套接字,并尝试连接到服务器的套接字地址。如果连接成功,服务器会接受这个连接并为客户端创建一个新的套接字,该套接字将用于与这个特定客户端之间的通信。
这样,服务器会保持一个主套接字用于监听客户端的连接请求,并为每个连接创建一个新的套接字来处理与特定客户端之间的通信。这些连接的套接字通常是独立的,即服务器和每个客户端之间都有一个独立的套接字,用于他们之间的通信。
UDP连接方式:
在 UDP 协议中,客户端并不需要显式地调用 bind() 来绑定一个端口。通常情况下,在客户端发送数据时,系统会自动分配一个临时的端口号,并在发送数据时使用这个端口号。这个临时端口号通常在发送后被释放,因此客户端不需要显式地绑定一个端口。
客户端在发送数据时,使用 sendto() 或者 sendmsg() 等函数向目标服务器发送数据报。在发送时,指定目标服务器的 IP 地址和端口号即可,而不需要调用 bind() 来指定客户端的本地端口。UDP是无连接的,因此客户端不需要事先建立连接,只需要在发送数据时指定目标地址和端口即可。
相反,服务器通常会先调用 bind() 来绑定一个固定的端口号,以便监听客户端发送来的数据。服务器需要绑定一个固定端口号来等待客户端的连接请求或者接收数据报。
总之,在UDP中,客户端通常不需要显式地调用 bind() 来绑定端口,它可以自动分配一个临时的端口来发送数据。服务器端则需要绑定一个固定的端口号来等待客户端的连接或接收数据。
区别:
TCP是面向连接的协议,它在通信之前需要建立连接,并确保数据传输的可靠性。它提供数据的可靠性保证、流量控制和拥塞控制。
UDP是无连接的协议,不需要在发送数据之前建立连接。它不保证数据的可靠性,也不提供类似TCP的可靠性保证机制。