首页 > 编程语言 >程序员面试题---------精细讲解DP协议编写网络程序以实现一个简单的加群和离群操作

程序员面试题---------精细讲解DP协议编写网络程序以实现一个简单的加群和离群操作

时间:2024-08-14 14:23:46浏览次数:8  
标签:linklist 面试题 函数 加群 data 节点 链表 --------- sin

基于UDP协议编写网络程序以实现一个简单的加群和离群操作:
假定:群组地址 (224.0.2.100) 服务器端地址为(192.168.14.44, 具体根据主机指定)
要求:
1.加群的成员(客户端) 加入一个群组后向管理者(服务器,地址公开)单播发送,“已加群”的消息,
2.管理者(服务器每收到一个成员的加群消息后,记录客户端地址信息,并在群里显示 xxxx已加群的消息 (组播);
3. 退群采用和加群相同的处理方式
4. 当成员加群和退群时,管理者要及时更新目前群成员信息,以便在群成语查看群成员。
客户端交互界面如下:

  1. 加入群聊
  2. 离开群聊
  3. 查看群成员
    ===========
    提示:服务器在记录客户端地址信息时,需要某种数据结构
    好的,让我们详细解释这段代码的每一部分。

1

在这里插入图片描述

1. 包含头文件

#include "myheader.h"

这行代码包含了自定义的头文件myheader.h。通常,这个头文件会包含网络编程所需的库,比如sys/socket.hnetinet/in.h等,以及一些自定义类型和宏的定义。

2. 主函数

int main(int argc, char** argv)

这是C语言程序的入口点。argc是命令行参数的数量,argv是一个指向参数字符串的指针数组。

3. 检查命令行参数

if(argc < 3)
{
    fprintf(stderr, "Usage <%s ServIP ServPort>\n", argv[0]);
    return -1;
}

这里检查用户是否提供了足够数量的命令行参数(IP地址和端口号)。如果没有,程序会打印使用方法并退出。

4. 创建套接字

int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd == -1)
{
    perror("socket");
    return -1;
}

程序使用socket函数创建一个UDP套接字。AF_INET表示IPv4地址族,SOCK_DGRAM表示数据报文服务,也就是UDP。如果创建失败,程序打印错误信息并退出。

5. 初始化变量

bool ismember = false;
sin_t server = {AF_INET};
sin_t any = {AF_INET};

这里定义了一个布尔变量ismember来跟踪用户是否是群组的成员。serverany是自定义的结构体变量,分别用于存储服务器地址信息和本地地址信息。

6. 设置服务器地址

server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);

将命令行参数中的端口号和IP地址转换为网络字节序,并存储在server结构体中。
在这里插入图片描述

7. 设置本地地址并绑定

any.sin_port = htons(12315);
any.sin_addr.s_addr = htonl(INADDR_ANY);
if(-1 == bind(sockfd, (sa_t*)&any, len2))
{
    perror("bind");
    close(sockfd);
    return -1;
}

将本地地址设置为任何可用的地址,并绑定到端口12315。如果绑定失败,程序打印错误信息,关闭套接字,并退出。

8. 初始化多播结构体

struct ip_mreqn multicast = {0};
multicast.imr_multiaddr.s_addr = inet_addr("224.0.2.100");
multicast.imr_address.s_addr = inet_addr("192.168.14.117");

初始化多播请求结构体multicast,指定多播组地址和本地接口地址。

9. 主循环

while(1)
{
    // 显示菜单并获取用户选择
    // ...
}

程序进入一个无限循环,显示一个菜单供用户选择不同的操作。

10. 处理用户选择

switch(index)
{
    case 1:
        // 加入群组
        // ...
        break;
    case 2:
        // 离开群组
        // ...
        break;
    case 3:
        // 查看成员
        // ...
        break;
    case 0:
        break; // 退出循环
}

在这里插入图片描述

根据用户的选择,执行相应的操作。以下是每个操作的详细解释:

加入群组
if(ismember)
{
    printf("你已经是群成员\n");
    continue;
}
if (-1 == setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &multicast, len))
{
    printf("加群失败!\n");
    continue;
}
ismember = true;
char *p = "已加群";
sendto(sockfd, p, strlen(p), 0, (sa_t*)&server, len);

如果用户还不是群组成员,则使用setsockopt加入多播组,并将ismember设置为true。然后发送一条消息给服务器。

离开群组
if(!ismember)
{
    printf("你已经不是群成员\n");
    continue;
}
if (-1 == setsockopt(sockfd, IPPROTO_IP, IP_DROP_MEMBERSHIP, &multicast, len))
    continue;
ismember = false;
char *p = "已退群";
sendto(sockfd, p, strlen(p), 0, (sa_t*)&server, len);

如果用户是群组成员,则使用setsockopt离开多播组,并将ismember设置为false。然后发送一条消息给
服务器。

查看成员
if(!ismember)
{
    printf("你不是群成员\n");
    continue;
}
char *p = "查看成员";
sendto(sockfd, p, strlen(p), 0, (sa_t*)&server, len);

如果用户是群组成员,则发送一条“查看成员”的消息给服务器。

11. 接收服务器响应

char buf[64] = {0};
int n = recvfrom(sockfd, buf, sizeof(buf)-1, 0, NULL, NULL);
if(n == -1)
    continue;
buf[n] = 0;
puts(buf);

在执行完上述操作后,程序使用recvfrom函数接收服务器发送的响应。如果接收成功,将接收到的数据存入buf数组,并在末尾添加空字符以形成字符串,然后打印出来。

12. 退出循环

如果用户选择退出程序(即选择0),break语句将执行,退出无限循环。

13. 关闭套接字并退出程序

close(sockfd);
return 0;

在退出循环后,程序关闭创建的套接字,并返回0,表示程序正常结束。

注意事项和潜在问题

  • sin_tsa_t是自定义类型,它们应该与struct sockaddr_instruct sockaddr兼容,这通常在myheader.h中定义。
  • lenlen2在使用时应该被设置为对应结构体的大小,通常通过sizeof操作符获取。
  • recvfrom函数的调用中没有检查返回值是否为-1以外的错误码,这可能会导致未处理的其他错误。
  • scanf函数的调用没有检查返回值,如果用户输入非数字字符,可能会导致未定义行为。
  • multicast结构体中的len字段在setsockopt调用中应该是sizeof(struct ip_mreqn),而不是lenlen2
  • 代码中没有处理可能的网络错误,例如sendtorecvfrom可能失败的情况。
  • 代码中没有使用非阻塞IO或超时设置,如果服务器没有响应,recvfrom可能会无限期地阻塞。
    这段代码是一个基础的网络编程示例,但在实际应用中,还需要考虑许多额外的错误处理和功能增强。

2

在这里插入图片描述

这段代码包含了三个函数,用于操作一个双向链表(尽管代码只展示了单向链表的行为)。以下是每个函数的详细解释:

1. linklist_append 函数

int linklist_append(linklist_t** head, data_t data)

这个函数用于在链表的末尾添加一个新的节点。

功能:
  • 分配一个新的链表节点pnew
  • 如果分配失败(pnewNULL),返回-1
  • 遍历链表,找到最后一个节点q
  • 将新节点pnew链接到链表的末尾。如果链表为空(qNULL),则新节点成为头节点。
  • 返回0表示成功。
    在这里插入图片描述
代码解释:
linklist_t *pnew = (linklist_t*)malloc(sizeof(linklist_t));
if(pnew == NULL)
    return -1;
linklist_t *p = *head, *q = NULL;
while(p)
{
    q = p;
    p = p -> next;
}
if(q)
    q -> next = pnew;
else
    *head = pnew;
return 0;

在这个函数中,pnew是新创建的节点,p是用于遍历链表的指针,q是跟踪p前一个节点的指针。通过这种方式,我们可以在链表的末尾添加一个新节点。

2. linklist_remove 函数

int linklist_remove(linklist_t** head, data_t data)

这个函数用于从链表中删除包含特定数据的节点。

功能:
  • 如果链表为空,返回-1
  • 如果头节点包含要删除的数据,则删除头节点并释放其内存。
  • 遍历链表,寻找包含要删除数据的节点。
  • 如果找到,将该节点从链表中移除并释放其内存。
  • 如果未找到,返回-1
代码解释:
if(*head == NULL)
    return -1;
linklist_t *p = *head, *q = NULL;
if(memcmp(&p -> data, &data, sizeof(data_t)) == 0)
{
    *head = p -> next;
    free(p);
    return 0;
}
while(p)
{
    if(memcmp(&p -> data, &data, sizeof(data_t)) == 0)
    {
        q -> next = p -> next;
        free(p);
        return 0;
    }
    q = p;
    p = p -> next;
}
return -1;

在这里插入图片描述
在这个函数中,p是当前遍历的节点,qp的前一个节点。memcmp函数用于比较两个数据块是否相等。如果找到匹配的数据,则将该节点从链表中移除。

3. linklist_free 函数

int linklist_free(linklist_t** head)

这个函数用于释放整个链表的内存。

功能:
  • 遍历链表,释放每个节点的内存。
  • 将头指针设置为NULL
代码解释:
linklist_t *p = *head, *q = NULL;
while(p)
{
    q = p;
    p = p -> next;
    free(q);
}
*head = NULL;
return 0;

在这个函数中,p是当前遍历的节点,q是即将释放的节点。通过这种方式,我们遍历整个链表,并逐个释放每个节点。

注意事项:

  • 这段代码假定linklist_tdata_t类型在linklist.h头文件中定义。
  • linklist_append函数没有初始化新节点的data字段,这应该在添加节点之前完成。
  • linklist_remove函数假定data_t类型可以通过memcmp直接比较,这可能不适用于所有数据类型。
  • linklist_remove函数没有释放找到的节点的内存,这是代码中的一个错误,应该使用free(p)来释放节点内存。
  • linklist_removelinklist_free函数中,应该检查free函数的返回值,尽管在大多数情况下free不会失败。
    这段代码是一个头文件定义,通常命名为linklist.h,它包含了链表操作的相关声明。以下是代码的详细解释:

1. 条件编译指令

#ifndef __LINKLIST_H
#define __LINKLIST_H

这是条件编译指令,用于防止头文件内容被重复包含。如果__LINKLIST_H没有被定义,则定义它,并包含下面的内容。如果已经定义了,则跳过下面的内容。

2. 包含其他头文件

#include "myheader.h"

这行代码包含了另一个名为myheader.h的头文件。这个头文件可能包含了其他必要的宏定义、类型定义或函数声明,它们对于链表操作可能是必需的。

3. 类型定义

typedef sin_t data_t;

这里定义了一个新的类型别名data_t,它被定义为sin_t类型。这表明链表节点存储的数据类型将与sin_t相同。sin_t可能是在myheader.h中定义的一个结构体。

4. 链表节点结构体定义

typedef struct _list 
{
    data_t data;
    struct _list *next;
} linklist_t;

这里定义了一个结构体_list,它代表链表的节点。结构体包含两个成员:

  • data:存储节点数据,类型为data_t
  • next:指向链表中下一个节点的指针。
    同时,linklist_t被定义为struct _list的别名,这样就可以使用linklist_t来引用链表节点类型。

5. 函数原型声明

以下是链表操作的函数原型声明:

int linklist_append(linklist_t**, data_t);

这个函数用于在链表的末尾添加一个新节点。它接受链表头指针的地址和一个data_t类型的值。

int linklist_remove(linklist_t**, data_t);

这个函数用于从链表中删除包含特定数据的节点。它也接受链表头指针的地址和一个data_t类型的值。

int linklist_free(linklist_t**);

这个函数用于释放整个链表的内存。它接受链表头指针的地址。
每个函数都返回一个int类型的值,通常用于指示函数执行成功与否(例如,成功时返回0,失败时返回-1)。
在这里插入图片描述

6. 结束条件编译指令

#endif

这行代码结束了条件编译指令。如果__LINKLIST_H已经被定义,则从#ifndef#endif之间的代码不会被包含。
这个头文件为链表操作提供了必要的类型和函数原型声明,使得其他源文件可以通过#include "linklist.h"来使用这些功能。需要注意的是,实际的函数实现应该在其他源文件中提供,并在编译时与包含这个头文件的源文件一起链接。
这段代码是一个头文件定义,通常命名为myheader.h,它包含了网络编程常用的头文件以及一些类型定义。以下是代码的详细解释:

1. 条件编译指令

#ifndef __MYHEADER_H
#define __MYHEADER_H

这是条件编译指令,用于防止头文件内容被重复包含。如果__MYHEADER_H没有被定义,则定义它,并包含下面的内容。如果已经定义了,则跳过下面的内容。

2. 包含标准库头文件

以下是一些标准库头文件的包含:

#include <unistd.h>

这包含了unistd.h,它提供了对POSIX操作系统API的访问,如fork()pipe()read()write()等。

#include <sys/types.h>

这包含了sys/types.h,它定义了各种系统数据类型,如size_tpid_toff_t等。

#include <sys/socket.h>

这包含了sys/socket.h,它提供了套接字API的定义,用于网络通信。

#include <netinet/in.h>

这包含了netinet/in.h,它提供了Internet协议族相关的定义,如struct sockaddr_in

#include <arpa/inet.h>

这包含了arpa/inet.h,它提供了用于网络地址转换的函数,如inet_addr()inet_ntoa()

#include <stdio.h>

这包含了stdio.h,它提供了标准输入输出库函数的定义,如printf()scanf()

#include <stdlib.h>

这包含了stdlib.h,它提供了标准库函数的定义,如内存分配malloc()free()以及程序控制exit()等。

#include <string.h>

这包含了string.h,它提供了字符串处理函数的定义,如strlen()strcpy()strcmp()等。

#include <stdbool.h>

这包含了stdbool.h,它提供了布尔类型bool的定义,以及truefalse常量的定义。

3. 类型定义

以下是对一些常用结构体类型的类型定义:

typedef struct sockaddr      sa_t;

这里定义了sa_t作为struct sockaddr的别名,struct sockaddr是一个通用的套接字地址结构。

typedef struct sockaddr_in   sin_t;

这里定义了sin_t作为struct sockaddr_in的别名,struct sockaddr_in是用于存储IPv4网络地址的结构。

4. 结束条件编译指令

#endif

在这里插入图片描述

这行代码结束了条件编译指令。如果__MYHEADER_H已经被定义,则从#ifndef#endif之间的代码不会被包含。
这个头文件为网络编程提供了必要的类型定义和头文件包含,使得其他源文件可以通过#include "myheader.h"来使用这些功能。通过这种方式,可以简化其他源文件中的代码,并确保所有相关的头文件都被正确地包含。
这段代码是一个使用C语言编写的UDP网络程序,它使用链表来管理加入和离开多播群组的客户端地址信息。以下是代码的详细解释:

1. 包含头文件

#include "myheader.h"
#include "linklist.h"

这两行代码分别包含了自定义的头文件myheader.hlinklist.hmyheader.h通常包含网络编程所需的库和类型定义,而linklist.h则包含链表操作的函数原型。

2. 主函数

int main(int argc, char** argv)

这是程序的入口点,argc是命令行参数的数量,argv是一个指向参数字符串的指针数组。

3. 检查命令行参数

if(argc < 3)
{
    fprintf(stderr, "Usage <%s ServIP ServPort>\n", argv[0]);
    return -1;
}

检查用户是否提供了足够数量的命令行参数(服务器IP地址和端口号)。如果没有,程序会打印使用方法并退出。

4. 初始化链表头指针

linklist_t *head = NULL;

初始化链表头指针为NULL

5. 创建套接字

int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd == -1)
{
    perror("socket");
    return -1;
}

使用socket函数创建一个UDP套接字。如果创建失败,程序打印错误信息并退出。

6. 设置多播地址和端口

sin_t multi = {AF_INET};
multi.sin_port = htons(12315);
multi.sin_addr.s_addr = inet_addr("224.0.2.100");

初始化一个sin_t结构体multi,用于存储多播地址和端口。

7. 设置服务器地址和端口

sin_t server = {AF_INET};
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);

初始化一个sin_t结构体server,并设置服务器地址和端口。

8. 绑定套接字

if(-1 == bind(sockfd, (sa_t*)&server, len))
{
    perror("bind");
    close(sockfd);
    return -1;
}

将套接字绑定到服务器地址和端口。如果绑定失败,程序打印错误信息,关闭套接字,并退出。

9. 主循环

while(1)
{
    // ...
}

程序进入一个无限循环,处理接收到的数据包。

10. 接收数据

sin_t client = {0};
int len = sizeof(sin_t);
char buf[64] = {0};
int n = recvfrom(sockfd, buf, sizeof(buf)-1, 0, (sa_t*)&client, &len);
if(n == -1)
    continue;
buf[n] = 0;

使用recvfrom函数接收客户端发送的数据包,并存储在buf数组中。client用于存储发送方的地址信息。

11. 处理接收到的数据

以下是针对不同消息内容的处理逻辑:

加入群组
if(strncmp(buf, "已加群", strlen("已加群")) == 0)
{
    linklist_append(&head, client);
    char join[64] = {0};
    sprintf(join, "[%s:%d]加入群组!", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
    sendto(sockfd, join, strlen(join), 0, (sa_t*)&multi, len2);
}

如果接收到的消息是"已加群",则将客户端地址信息添加到链表中,并发送一条加入群组的消息到多播地址。

离开群组
else if(strncmp(buf, "已退群", strlen("已退群")) == 0)
{
    linklist_remove(&head, client);
    char leave[64] = {0};
    sprintf(leave, "[%s:%d]离开群组!", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
    sendto(sockfd, leave, strlen(leave), 0, (sa_t*)&multi, len2);
}

如果接收到的消息是"已退群",则从链表中删除客户端地址信息,并发送一条离开群组的消息到多播地址。

查看成员
else if(strncmp(buf, "查看成员", strlen("查看成员")) == 0)
{
    linklist_t *p = head;
    char memlist[1024] = {```c
    char memlist[1024] = {0};
    while(p)
    {
        char member[64] = {0};
        sprintf(member, "[%s:%d]\n", inet_ntoa(p->data.sin_addr), ntohs(p->data.sin_port));
        strcat(memlist, member);
        p = p->next;
    }
    sendto(sockfd, memlist, strlen(memlist), 0, (sa_t*)&multi, len2);
}

如果接收到的消息是"查看成员",则遍历链表,将所有成员的地址信息构建成字符串,并发送到多播地址。

12. 关闭套接字

close(sockfd);

在处理完所有接收到的数据包后,关闭套接字。

13. 程序结束

return 0;

程序返回0,表示成功结束。

注意事项和潜在问题:

  • 代码中使用了strncmp函数来比较字符串,这可能会导致安全问题,例如缓冲区溢出。建议使用strcmpstrcasecmp来比较字符串。
  • 代码中使用了inet_ntoantohs函数来转换地址和端口,这些函数可能会在不同的系统上有所不同。建议使用更通用的方法,如inet_ptonhtons
  • 代码中使用了sprintf函数来格式化字符串,这可能会导致缓冲区溢出。建议使用更安全的函数,如snprintf
  • 代码中使用了recvfromsendto函数来接收和发送数据包,这些函数可能会在不同的系统上有所不同。建议使用更通用的函数,如recvfromsendto
  • 代码中使用了memlist数组来存储成员列表,这可能会导致内存泄漏。建议使用动态分配的内存来存储成员列表。
  • 代码中没有处理可能的网络错误,例如recvfromsendto可能失败的情况。
  • 代码中没有使用非阻塞IO或超时设置,如果服务器没有响应,recvfrom可能会无限期地阻塞。
    这段代码是一个基础的网络编程示例,但在实际应用中,还需要考虑许多额外的错误处理和功能增强。

在这里插入图片描述

标签:linklist,面试题,函数,加群,data,节点,链表,---------,sin
From: https://blog.csdn.net/yuanbenshidiaos/article/details/141189718

相关文章

  • 0228-TCP 的标记和选项
    环境Time2022-11-24WSL-Ubuntu22.04Rust1.65.0pnet0.31.0tun-tap0.1.3前言说明参考:https://docs.rs/pnet/latest/pnet/index.html参考:https://www.cnblogs.com/lshs/p/6038494.html目标了解TCP协议头中的flags和options字段的含义。main.rsusepnet::pa......
  • 0229-UDP 发送和接收
    环境Time2022-11-25WSL-Ubuntu22.04Rust1.65.0前言说明参考:https://doc.rust-lang.org/std/net/struct.UdpSocket.html目标之前通过接收整个IP和UDP报文来实现了通信,这里去除报文头的细节,直接通信。main.rsUDP由标准库直接支持,可以直接使用。将发送过来的信......
  • 0230-TCP 发送和接收
    环境Time2022-11-25WSL-Ubuntu22.04Rust1.65.0前言说明参考:https://doc.rust-lang.org/std/net/struct.TcpListener.html目标之前认识了TCP报文头,这里去除报文头的细节,直接通信。main.rsTCP由标准库直接支持,可以直接使用。连接上去后,服务器返回一个hello。us......
  • 0226-UDP 请求报文
    环境Time2022-11-20WSL-Ubuntu22.04Rust1.65.0pnet0.31.0tun-tap0.1.3前言说明参考:https://docs.rs/pnet/latest/pnet/index.htmlRFC792目标了解UDP协议的的字段。从这里开始,将进入第四层,传输层协议。UDP协议基于IP协议。配置TUNIP地址不要和主机的......
  • 0227-TCP 请求报文
    环境Time2022-11-24WSL-Ubuntu22.04Rust1.65.0pnet0.31.0tun-tap0.1.3前言说明参考:https://docs.rs/pnet/latest/pnet/index.htmlRFC793目标了解TCP协议头中的字段,其也是基于IP协议的。配置TUNIP地址不要和主机的网卡地址在一个段,以便选择这个网卡进行......
  • 每日一题:Leetcode-662 二叉树最大宽度
    力扣题目解题思路java代码力扣题目:给你一棵二叉树的根节点 root ,返回树的 最大宽度 。树的 最大宽度 是所有层中最大的 宽度 。每一层的 宽度 被定义为该层最左和最右的非空节点(即,两个端点)之间的长度。将这个二叉树视作与满二叉树结构相同,两端点间会出现一些......
  • Java面试题(Java Web)
    目录1.JSP有哪些内置对象?作用分别是什么?2.说一下JSP的4种作用域?3.session和cookie有什么区别?4.说一下session的工作原理?5.如果客户端禁止cookie能实现session还能用吗?6.如何避免SQL注入?7.什么是XSS攻击,如何避免?8.什么是CSRF攻击,如何避免?......
  • Java面试题(异常)
    目录1.throw和throws的区别?2.final、finally、finalize有什么区别?3.try-catch-finally中哪个部分可以省略?4.try-catch-finally中,如果catch中return了,finally还会执行吗?5.常见的异常类有哪些?1.throw和throws的区别?throw:是真实抛出一个异常。thr......
  • Java面试题(网络)
    1.forward和redirect的区别?forward是转发和redirect是重定向:地址栏url显示:fowardurl不会发生改变,redirecturl会发生改变;数据共享:      forward可以共享request里的数据,redirect不能共享;效       率:      forw......
  • 开关资源新方法:Try- with-resources
    JDK7新特性:Try-with-resourcestry-with-resources是JDK7中引入的一种新的异常处理机制,它主要用于自动管理资源,能够很容易地关闭在try-catch语句块中使用的资源。确保资源在不再需要时能够被正确关闭。这种机制简化了资源管理,使得资源的释放更加安全和可预测。resource:是......