项目:UDP聊天室


UDP

UDP(User Datagram Protocol)是一种无连接、不可靠、面向数据报的传输协议。与TCP相比,UDP更加轻量级,不提供像TCP那样的可靠性和流控制机制,但具备较低的通信延迟和较少的开销。

UDP具有以下几个特点:

  1. 无连接性:UDP在通信之前不需要进行握手或建立连接,可以直接向目标主机发送数据报。这使得UDP的开销较低,适用于实时数据传输或需要快速响应的应用场景。

  2. 不可靠性:UDP不保证数据报的可靠性传输,发送端一旦发送数据包就不会去确认是否到达目标主机。这意味着UDP数据包可能会在传输过程中丢失、重复、乱序,接收方需要自行处理这些问题。

  3. 面向数据报:UDP将应用层交给它的数据封装成单个数据报发送,每个数据报都是独立的。接收方以数据报为单位进行处理,不会像TCP一样存在拆包和粘包的问题。

  4. 快速:由于UDP没有连接的建立和断开过程,且不需要进行可靠性的保证,因此UDP的通信延迟较低。这使得UDP适用于需要实时性和高性能的应用,例如音视频传输、实时游戏等。

UDP常见的应用场景包括以下几个方面:

  1. 实时数据传输:UDP适合用于实时数据传输,如音视频流、实时视频会议等。由于UDP的低延迟和快速性能,可以保证数据的及时到达,并避免了TCP的可靠性机制带来的可能的延迟。

  2. DNS(Domain Name System):UDP常用于DNS查询,客户端通过UDP向DNS服务器发送域名解析请求,DNS服务器返回响应信息。

  3. 移动应用:UDP适合用于移动应用,如移动终端上的实时定位、实时数据传输等场景。由于UDP不需要进行连接的建立和断开,更加适应移动网络环境的不稳定性。

需要注意的是,由于UDP不提供可靠性保证,需要在应用层面上考虑数据的可靠性和完整性,例如通过应用层协议、数据校验和重传机制等手段来确保数据的正确传输。

要求

利用UDP协议,实现一套聊天室软件。服务器端记录客户端的地址,客户端发送消息后,服务器群发给各个客户端软件

问题思考:

l 客户端会不会知道其它客户端地址?

UDP客户端不会直接互连,所以不会获知其它客户端地址,所有客户端地址存储在服务器端。

l 有几种消息类型?

登录:服务器存储新的客户端的地址。把某个客户端登录的消息发给其它客户端。

聊天:服务器只需要把某个客户端的聊天消息转发给所有其它客户端。 退出:服务器删除退出客户端的地址,并把退出消息发送给其它客户端。

l 服务器如何存储客户端的地址?

数据结构使用链表最为简单

流程图

服务器

客户端

思路:

1.首先要实现客户端和服务器端的全双工通信,因此要用到多进程,多线程或IO多路复用 ,IO多路复用包括select,poll,epoll。其中epoll效率最高,支持百万级别的并发。

三种IO多路复用特点以及流程如下

2.服务器采用有头单向链表,存储每个客户端的IP地址,链表节点结构体如下。

cpp 复制代码
/* 链表结点结构体 */
typedef struct node_t
{
    struct sockaddr_in addr;   //IP地址结构体
    struct node_t *next;       //指针域
}node_t;

3.建立传送消息的结构体,结构体分为三部分,分别是消息类型,昵称和消息正文。服务器端根据判断消息类型进行不同的操作。消息结构体如下。

/消息对应的结构体(同一个协议)/

cpp 复制代码
typedef struct msg_t
{
    int type;       //L登录  M聊天  Q退出
    char name[32];  //用户名
    char text[128]; //消息正文
} MSG_t;

4.客户端运行时,首先发送链接类型信息,进行链接。链接后发送信息,根据信息内容判断是否退出。

实现

此项目可以使用多进程,多线程,select,poll,epoll实现,此次我会用 多线程和epoll两种方式实现。其余方法相差不大。

多进程实现

head.h

cpp 复制代码
#ifndef __HEAD_H__
#define __HEAD_H__
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <semaphore.h>

/* 链表结点结构体 */
typedef struct node_t
{
    struct sockaddr_in addr;   //IP地址结构体
    struct node_t *next;       //指针域
}node_t;

/* 消息对应的结构体(同一个协议) */
typedef struct msg_t
{
    int type;       //L登录  M聊天  Q退出
    char name[32];  //用户名
    char text[128]; //消息正文
} MSG_t;


enum type_t
{
  login,      //枚举后面要用逗号隔开  ---0
  message,    // ---1
  quit,       // ---2 
};
#endif

server.c

cpp 复制代码
#include "head.h"
struct sockaddr_in saddr, caddr;
//创建链表
node_t *CreateList()
{
    node_t *p = (node_t *)malloc(sizeof(node_t));
    if (NULL == p)
    {
        perror("CreateList is err");
        return NULL;
    }
    p->next = NULL;
    return p;
}

//客户端链接
void server_link(int socked, node_t *p, MSG_t *msg)
{
    sprintf(msg->text, "%s连接", msg->name);
    printf("%s\n", msg->text);
    while (p->next)
    {
        p = p->next;
        sendto(socked, msg, sizeof(*msg), 0, (struct sockaddr *)(&(p->addr)), sizeof(p->addr));
    }
    node_t *new = (node_t *)malloc(sizeof(node_t));
    new->next = NULL;
    new->addr=caddr;
    p->next = new;
}

//客户端发送正文消息
void server_message(int socked, node_t *p, MSG_t *msg)
{
    while (p->next)
    {
        p = p->next;
        if ((memcmp(&(p->addr), &caddr, sizeof(caddr))) == 0)
            continue;
        sendto(socked, msg, sizeof(*msg), 0, (struct sockaddr *)(&(p->addr)), sizeof(p->addr));
    }
}

//客户端退出
void server_quit(int socked, node_t *p, MSG_t *msg)
{
    node_t *pdel = NULL;
    sprintf(msg->text, "%s退出", msg->name);
    printf("%s\n", msg->text);
    while (p->next)
    {
        if ((memcmp(&(p->next->addr), &caddr, sizeof(caddr))) == 0)
        {
            pdel = p->next;
            p->next = pdel->next;
            free(pdel);
            pdel = NULL;
        }
        else
        {
            p = p->next;
            sendto(socked, msg, sizeof(*msg), 0, (struct sockaddr *)(&(p->addr)), sizeof(p->addr));
        }
    }
}

//6.服务器端发送消息,所有客户端接收
void *mypthread(void *socked)
{
    MSG_t msg;
    msg.type = message;
    strcpy(msg.name, "服务器");
    while (1)
    {

        fgets(msg.text, sizeof(msg.text), stdin);
        if (msg.text[strlen(msg.text) - 1] == '\n')
            msg.text[strlen(msg.text) - 1] = '\0';
        sendto(*(int *)(socked), &msg, sizeof(msg), 0, (struct sockaddr *)(&saddr), sizeof(saddr));
    }
    pthread_exit(NULL);
}

int main(int argc, char const *argv[])
{
    //1.建立socket套接字
    int socked = socket(AF_INET, SOCK_DGRAM, 0);
    if (socked < 0)
    {
        perror("socket is err");
        return -1;
    }

    //2.bind服务器端口号和ip地址
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(atoi(argv[1]));
    saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
    int len = sizeof(caddr);
    if ((bind(socked, (struct sockaddr *)(&saddr), sizeof(saddr))) < 0)
    {
        perror("bind is err");
        return -1;
    }

    //3.建立多进程,主进程服务器接收,一个服务器发送
    pthread_t pid;
    pthread_create(&pid, NULL, mypthread, &socked);
    pthread_detach(pid);

    //4.主线程不断接收客户端发送的消息
    node_t *p = CreateList();
    MSG_t msg;
    while (1)
    {
        if ((recvfrom(socked, &msg, sizeof(msg), 0, (struct sockaddr *)(&caddr), &len)) < 0)
        {
            perror("recvfrom is err");
            return -1;
        }

        //5.根据消息类型进行分类讨论
        switch (msg.type)
        {
        case login:
            server_link(socked, p, &msg);
            break;
        case message:
            server_message(socked, p, &msg);
            break;
        case quit:
            server_quit(socked, p, &msg);
            break;
        default:
            break;
        }
    }
    close(socked);
    return 0;
}

client.c

cpp 复制代码
#include "head.h"
struct sockaddr_in saddr, caddr;
int len;
//5.子线程接收客户端数据
void *mypthread(void *socked)
{
    MSG_t msg;
    while (1)
    {
        int temp = recvfrom(*(int *)(socked), &msg, sizeof(msg), 0, (struct sockaddr *)(&caddr), &len);
        if (temp < 0)
        {
            perror("recvfrom err");
            exit(-1);
        }
        else
        {
            printf("\t\t\t\t%s(%s)\n",msg.text,msg.name);
        }
    }
}

int main(int argc, char const *argv[])
{
    //1.建立socket套接字
    int socked = socket(AF_INET, SOCK_DGRAM, 0);
    if (socked < 0)
    {
        perror("socket is err");
        return -1;
    }

    //2.绑定服务器ip和端口号
    saddr.sin_port = htons(atoi(argv[2]));
    saddr.sin_addr.s_addr = inet_addr(argv[1]);
    saddr.sin_family = AF_INET;
    len = sizeof(caddr);

    //3.连接服务器
    MSG_t msg;
    msg.type = login;
    printf("请输入名称:\n");
    fgets(msg.name, sizeof(msg.name), stdin);
    if (msg.name[strlen(msg.name) - 1] == '\n')
        msg.name[strlen(msg.name) - 1] = '\0';
    sendto(socked, &msg, sizeof(msg), 0, (struct sockaddr *)(&saddr), sizeof(saddr));

    //4.创建子进程接收服务器数据,主进程发送数据
    pthread_t pid;
    pthread_create(&pid, NULL, mypthread, &socked);
    pthread_detach(pid);

    //5.主线程发送客户端数据
    while (1)
    {
        fgets(msg.text, sizeof(msg.text), stdin);
        if (msg.text[strlen(msg.text) - 1] == '\n')
            msg.text[strlen(msg.text) - 1] = '\0';
        if (strcmp(msg.text, "quit") == 0)
            msg.type = quit;
        else
            msg.type = message;
        sendto(socked, &msg, sizeof(msg), 0, (struct sockaddr *)(&saddr), sizeof(saddr));
    }
    close(socked);
    return 0;
}

Makefile

cpp 复制代码
all:
    gcc client.c -o client -lpthread
    gcc server.c -o server -lpthread
.PHONY:clean
clean:
    rm *.o

epoll实现

head.h

cpp 复制代码
#ifndef __HEAD_H__
#define __HEAD_H__
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <semaphore.h>
#include <sys/epoll.h>

/* 链表结点结构体 */
typedef struct node_t
{
    struct sockaddr_in addr;   //IP地址结构体
    struct node_t *next;       //指针域
}node_t;

/* 消息对应的结构体(同一个协议) */
typedef struct msg_t
{
    int type;       //L登录  M聊天  Q退出
    char name[32];  //用户名
    char text[128]; //消息正文
} MSG_t;


enum type_t
{
  login,      //枚举后面要用逗号隔开  ---0
  message,    // ---1
  quit,       // ---2 
};
#endif

server.c

cpp 复制代码
#include "head.h"
struct sockaddr_in saddr, caddr;
//创建链表
node_t *CreateList()
{
    node_t *p = (node_t *)malloc(sizeof(node_t));
    if (NULL == p)
    {
        perror("CreateList is err");
        return NULL;
    }
    p->next = NULL;
    return p;
}

//客户端链接
void server_link(int socked, node_t *p, MSG_t *msg)
{
    sprintf(msg->text, "%s连接", msg->name);
    printf("%s\n", msg->text);
    while (p->next)
    {
        p = p->next;
        sendto(socked, msg, sizeof(*msg), 0, (struct sockaddr *)(&(p->addr)), sizeof(p->addr));
    }
    node_t *new = (node_t *)malloc(sizeof(node_t));
    new->next = NULL;
    new->addr = caddr;
    p->next = new;
}

//客户端发送正文消息
void server_message(int socked, node_t *p, MSG_t *msg)
{
    while (p->next)
    {
        p = p->next;
        if ((memcmp(&(p->addr), &caddr, sizeof(caddr))) == 0)
            continue;
        sendto(socked, msg, sizeof(*msg), 0, (struct sockaddr *)(&(p->addr)), sizeof(p->addr));
    }
}

//客户端退出
void server_quit(int socked, node_t *p, MSG_t *msg)
{
    node_t *pdel = NULL;
    sprintf(msg->text, "%s退出", msg->name);
    printf("%s\n", msg->text);
    while (p->next)
    {
        if ((memcmp(&(p->next->addr), &caddr, sizeof(caddr))) == 0)
        {
            pdel = p->next;
            p->next = pdel->next;
            free(pdel);
            pdel = NULL;
        }
        else
        {
            p = p->next;
            sendto(socked, msg, sizeof(*msg), 0, (struct sockaddr *)(&(p->addr)), sizeof(p->addr));
        }
    }
}


int main(int argc, char const *argv[])
{
    //1.建立socket套接字
    int socked = socket(AF_INET, SOCK_DGRAM, 0);
    if (socked < 0)
    {
        perror("socket is err");
        return -1;
    }

    //2.bind服务器端口号和ip地址
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(atoi(argv[1]));
    saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
    int len = sizeof(caddr);
    if ((bind(socked, (struct sockaddr *)(&saddr), sizeof(saddr))) < 0)
    {
        perror("bind is err");
        return -1;
    }

   
    //3.主线程不断接收客户端发送的消息
    node_t *p = CreateList();
    MSG_t msg;

    //1).创建红黑树以及链表
    //树的跟节点/树的句柄
    int epfd = epoll_create(1);
    //2).上树
    struct epoll_event event;
    struct epoll_event events[32];
    event.events = EPOLLET | EPOLLIN;

    event.data.fd = 0;
    epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &event);

    event.data.fd = socked;
    epoll_ctl(epfd, EPOLL_CTL_ADD, socked, &event);

    while (1)
    {

        //3).阻塞等待文件描述符产生事件
        int ret = epoll_wait(epfd, events, 32, -1);
        if (ret < 0)
        {
            perror("epoll err");
            return -1;
        }
        //4).根据文件描述符号,进行处理
        for (int i = 0; i < ret; ++i)
        {
            if (events[i].data.fd == 0)
            {

                msg.type = message;
                strcpy(msg.name, "客户端");
                fgets(msg.text, sizeof(msg.text), stdin);
                if (msg.text[strlen(msg.text) - 1] == '\n')
                    msg.text[strlen(msg.text) - 1] = '\0';
                sendto(*(int *)(socked), &msg, sizeof(msg), 0, (struct sockaddr *)(&saddr), sizeof(saddr));
            }
            else if (events[i].data.fd == socked)
            {
                if ((recvfrom(socked, &msg, sizeof(msg), 0, (struct sockaddr *)(&caddr), &len)) < 0)
                {
                    perror("recvfrom is err");
                    return -1;
                }

                //4.根据消息类型进行分类讨论
                switch (msg.type)
                {
                case login:
                    server_link(socked, p, &msg);
                    break;
                case message:
                    server_message(socked, p, &msg);
                    break;
                case quit:
                    server_quit(socked, p, &msg);
                    break;
                default:
                    break;
                }
            }
        }
    }
    close(socked);
    return 0;
}

client.c

cpp 复制代码
#include "head.h"
struct sockaddr_in saddr, caddr;
int len;
//5.子线程接收客户端数据
void *mypthread(void *socked)
{
    MSG_t msg;
    while (1)
    {
        int temp = recvfrom(*(int *)(socked), &msg, sizeof(msg), 0, (struct sockaddr *)(&caddr), &len);
        if (temp < 0)
        {
            perror("recvfrom err");
            exit(-1);
        }
        else
        {
            printf("\t\t\t\t%s(%s)\n", msg.text, msg.name);
        }
    }
}

int main(int argc, char const *argv[])
{
    //1.建立socket套接字
    int socked = socket(AF_INET, SOCK_DGRAM, 0);
    if (socked < 0)
    {
        perror("socket is err");
        return -1;
    }

    //2.绑定服务器ip和端口号
    saddr.sin_port = htons(atoi(argv[2]));
    saddr.sin_addr.s_addr = inet_addr(argv[1]);
    saddr.sin_family = AF_INET;
    len = sizeof(caddr);

    //3.连接服务器
    MSG_t msg;
    msg.type = login;
    printf("请输入名称:\n");
    fgets(msg.name, sizeof(msg.name), stdin);
    if (msg.name[strlen(msg.name) - 1] == '\n')
        msg.name[strlen(msg.name) - 1] = '\0';
    sendto(socked, &msg, sizeof(msg), 0, (struct sockaddr *)(&saddr), sizeof(saddr));

    //4.创建子进程接收服务器数据,主进程发送数据
    pthread_t pid;
    pthread_create(&pid, NULL, mypthread, &socked);
    pthread_detach(pid);

    //5.主线程发送客户端数据
    while (1)
    {
        fgets(msg.text, sizeof(msg.text), stdin);
        if (msg.text[strlen(msg.text) - 1] == '\n')
            msg.text[strlen(msg.text) - 1] = '\0';
        if (strcmp(msg.text, "quit") == 0)
        {
            msg.type = quit;
            sendto(socked, &msg, sizeof(msg), 0, (struct sockaddr *)(&saddr), sizeof(saddr));
            break;
        }

        else
            msg.type = message;
        sendto(socked, &msg, sizeof(msg), 0, (struct sockaddr *)(&saddr), sizeof(saddr));
    }

    return 0;
}

Makefile

cpp 复制代码
all: 
    gcc client.c -o client -lpthread 
    gcc server.c -o server -lpthread
 .PHONY:clean 
    clean: rm *.o
相关推荐
绵绵细雨中的乡音1 小时前
网络基础知识
linux·网络
Peter·Pan爱编程2 小时前
Docker在Linux中安装与使用教程
linux·docker·eureka
想睡hhh2 小时前
网络基础——协议认识
网络·智能路由器
kunge20132 小时前
Ubuntu22.04 安装virtualbox7.1
linux·virtualbox
清溪5492 小时前
DVWA中级
linux
Sadsvit3 小时前
源码编译安装LAMP架构并部署WordPress(CentOS 7)
linux·运维·服务器·架构·centos
xiaok3 小时前
为什么 lsof 显示多个 nginx 都在 “使用 443”?
linux
苦学编程的谢4 小时前
Linux
linux·运维·服务器
G_H_S_3_4 小时前
【网络运维】Linux 文本处理利器:sed 命令
linux·运维·网络·操作文本
Linux运维技术栈4 小时前
多系统 Node.js 环境自动化部署脚本:从 Ubuntu 到 CentOS,再到版本自由定制
linux·ubuntu·centos·node.js·自动化