S/C模型(上),利用UDP广播,TCP协议和多线程实现client的局域网设备搜索与实时通信连接

一、S/C模型主要功能流程图

        模型的主要功能基本如上图所示,对于client的设计而言,主要分为两个部分,一是UDP服务器的创建,然后进行广播数据的发送搜寻局域网设备并对回应设备的IP进行接收,二是TCP对回应设备IP的定向连接与数据的收发。

二、client的UDP搭建部分

2.1广播发送

        这里注意的一点是:广播数据是设计用来在子网内的所有主机上接收。当一台设备发送一个广播消息时,这个消息会被子网内所有设备接收,无论它们是否请求或需要这些数据。

2.1.1套接字创建

/*****************************************************************************
 函数原型  : int socket( int af, int type, int protocol)
 功能描述  : 在操作系统中创建一个新的套接字
             套接字是网络通信的端点,它为不同主机之间的进程提供了一种通信机制

 输入参数  : int af:    (Address Family)地址族表示通信协议类型,AF_INET表示通信协议为ipv4
            int type:    表示Socket类型,SOCK_DGRAM为数据报形式
            int protocol:    表示传输协议,0表示系统默认自动推演;type为 SOCK_DGRAM时,该参数为IPPROTO_UDP,指定使用 UDP 协议
 输出参数  : 无
 返 回 值  : int    成功返回非负值,表示套接字的文件描述符,失败返回-1

*****************************************************************************/
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
    {
        perror("socket error\n");
        exit(EXIT_FAILURE);
    }
printf("Socket is created successfully\n");

2.1.2套接字绑定

        如果只是单纯地发送发送广播数据,这一步是没有必要的,但是我们还要接收服务器回传的ip地址,所以便需要 bind() 特定端口进行监听。

/*****************************************************************************
 函数原型  : int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
 功能描述  : 关联套接字与特定的网络地址和端口号
             确保数据能够正确地发送和接收到正确的网络接口和应用程序。

 输入参数  : int sockfd:    已经创建的套接字文件描述符
            const struct sockaddr *addr:    指向 sockaddr 结构体的指针,该结构体包含了套接字要绑定的地址信息
                    struct sockaddr_in {
                        sa_family_t    sin_family;   // 地址族
                        in_port_t      sin_port;     // 端口号
                        struct in_addr sin_addr;     // 网络地址,需要网络字节序列
                        unsigned char  sin_zero[8];  // 填充,确保结构体长度,为了跟sockaddr结构在内存中对齐
                    }
             socklen_t addrlen:    为addr 变量的大小,可由 sizeof() 计算得出。
 输出参数  : 无
 返 回 值  : int    成功返回0,失败返回-1

*****************************************************************************/
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));    //初始化清零内存,避免随机或残存
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定所有接口。宏INADDR_ANY为0.0.0.0
//servaddr.sin_addr.s_addr = inet_addr("192.168.1.100");    // 将点分十进制 IPv4 地址字符串转换为网络字节序(大端字节序)
server_addr.sin_port = htons(8989);              // 转换为网络字节序(host to net signed long)

if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
{
    perror("Bind failed\n");
    close(sockfd);
    exit(EXIT_FAILURE);
}
printf("Socket is bound successfully\n");

2.1.3设置广播开关与地址信息

/*****************************************************************************
 函数原型  : int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
 功能描述  : 用于设置套接字选项,这些选项可以控制套接字的行为。

 输入参数  : sockfd:套接字文件描述符,即你想要设置选项的套接字。
            level:协议层,指定选项所属的协议层,常见的值有 SOL_SOCKET(通用套接字选项)
            optname:选项名称,指定要设置的选项。
                    常用套接字选项:
                        SO_REUSEADDR:允许套接字绑定到一个正在使用中的地址上,通常用于开发阶段,以避免因套接字仍在 TIME_WAIT 状态而导致的地址不可用问题。
                        SO_BROADCAST:允许套接字发送广播消息。
                        SO_KEEPALIVE:启用 TCP 心跳,以保持连接的活跃状态。
                        SO_SNDBUF 和 SO_RCVBUF:设置发送和接收缓冲区的大小。
                        SO_LINGER:控制套接字关闭时的行为,可以设置一个延迟时间,使套接字在关闭前等待未完成的发送操作完成。
            optval:指向选项值的指针,这个值的具体类型和内容取决于 optname。
            optlen:选项值的长度,以字节为单位。

 输出参数  : 无
 返 回 值  : int    成功返回0,表示套接字的文件描述符,失败返回-1

*****************************************************************************/
// 设置套接字选项,允许发送广播消息
int broadcastPermission = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &broadcastPermission, sizeof(broadcastPermission)) < 0)
{
    perror("setsockopt failed\n");
    close(sockfd);
    exit(EXIT_FAILURE);
}

// 填充服务器信息
struct sockaddr_in broadcastAddr;
memset(&broadcastAddr, 0, sizeof(broadcastAddr));
broadcastAddr.sin_family = AF_INET;
broadcastAddr.sin_addr.s_addr = INADDR_BROADCAST;    //宏INADDR_BROADCAST为255.255.255.255
broadcastAddr.sin_port = htons(8989);
printf("UDP broadcast server is running\n");

2.1.4发送广播数据

/*****************************************************************************
 函数原型  : ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);
 功能描述  : 用于向特定的网络地址发送数据

 输入参数  : sockfd:    套接字文件描述符,即你想要通过它发送数据的套接字。
            buf:    指向要发送数据缓冲区的指针。
            len:    要发送的数据的长度。
            flags:    通常设置为 0,用于指定发送操作的行为。可以设置特定的标志来改变发送行为,如 MSG_DONTROUTE(不通过路由发送)。
            dest_addr:    指向 sockaddr 结构的指针,该结构指定了接收方的地址。
            addrlen:    dest_addr 结构的长度。
 输出参数  : 无
 返 回 值  : int(32位机)/long int    成功时返回发送的字节数,失败时返回 -1 

*****************************************************************************/
int stopbroadcast = 0;    //在函数外设置全局变量stopbroadcast

char *message = "Hello world!";
while (1)
{
    int sendsize = sendto(sockfd, message, strlen(message), 0, (struct sockaddr *)&broadcastAddr, sizeof(broadcastAddr));
    if (sendsize < 0)
    {
        perror("sendto failed\n");
        close(sockfd);
        exit(EXIT_FAILURE);
    }
    else
        printf("Is anyone there?\n");

    if (stopbroadcast == 1)   //设置出口
    {
        printf("UDP broadcast ends\n");
        break;
    }
    sleep(1);    //避免过载
}

2.2线程接收

        既然广播数据的发送有了,那UDP服务端如何在不退出数据发送循环的基础上接收返回设备的IP信息呢?如何确保其他设备回传时能及时收到该信息呢?答案是创建一个多线程进行IP接收。

2.2.1线程接收函数实现

/*****************************************************************************
 函数原型  : void *receive_thread(void *arg)
 功能描述  : 接收子网内其他特定设备回传的IP等信息并打印

 输入参数  : void *arg:    本机套接字socket的文件描述符
 输出参数  : 无
 返 回 值  : void *    返回接收设备回传的IP

*****************************************************************************/
void *udp_receive_thread(void *arg)
{
    int sock_fd = *(int *)arg;
    char recv_buf[1024];
    char password[] = "I am here!";
    struct sockaddr_in recv_addr;
    memset(&recv_addr, 0, sizeof(recv_addr));
    socklen_t addrlen = sizeof(recv_addr);
    printf("receive start\n");
    while (1)
    {
        memset(recv_buf, 0, sizeof(recv_buf));
        //recvfrom与sendto参数基本一致,唯一不同是最后一个参数为socklen_t *addrlen
        int ret = recvfrom(sock_fd, recv_buf, sizeof(recv_buf), 0, (struct sockaddr *)&recv_addr, &addrlen);
        // 确保字符串正确结束,网络协议不关心数据类型
        recv_buf[ret] = '\0'; 
        if (ret > 0 && strcmp(recv_buf, password) == 0)
        {
            // inet_ntoa将网络地址转换为点分十进制字符串
            printf(" cliIP =%s, cliPort =%d, recv message =[%s]\n",
                        inet_ntoa(recv_addr.sin_addr), recv_addr.sin_port, recv_buf);
            // 提示结束广播数据发送
            stopbroadcast = 1;
            break;
        }
    }
    pthread_exit(inet_ntoa(recv_addr.sin_addr));
}

2.2.2线程创建与返回值接收

/*****************************************************************************
 函数原型  : int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
 功能描述  : POSIX 线程库中用于创建新线程

 输入参数  : thread:    用于存储新创建线程的标识符。
            attr:    用于指定线程属性。如果设置为 NULL,则新线程将使用默认属性。
            start_routine:    这是新线程开始执行的函数。它必须是一个返回 void* 并接受 void* 参数的函数。
            arg:    这是传递给 start_routine 函数的参数。
 输出参数  : 无
 返 回 值  : int    成功返回0,失败返回错误代码

*****************************************************************************/

// 创建多线程
int *sock_tmp = &sockfd;
pthread_t tid;
int ret = pthread_create(&tid, NULL, udp_receive_thread, (void *)sock_tmp);
if (ret != 0) 
{
    perror("pthread_create failed\n");
    close(sockfd);
    exit(EXIT_FAILURE);
}

//接收线程返回的ip
void *linkIP;
//int pthread_join(pthread_t thread, void **retval);
if (pthread_join(tid, &linkIP) != 0)
{
    perror("pthread_join error\n");
    close(sockfd);
    exit(EXIT_FAILURE);
}
else
    printf("linkIP: %s\n", (char *)linkIP);

        在获得子网内其他设备的ip地址后,Client的UDP部分就宣告结束了,那么接下就要进入TCP的定向连接和数据收发了。

三、Client的TCP搭建部分

        对于client客户端,TCP的搭建可以主要分为三个线程,一个线程用来接收数据,一个线程用来发送数据,最后一个主线程用来创建TCP连接和上述收发线程。

3.1连接建立

/*****************************************************************************
 函数原型  : int TCPlink(char *linkIP)
 功能描述  : 建立TCP连接,创建收发线程与线程回收

 输入参数  : char *linkIP:    要连接的IP地址
 输出参数  : 无
 返 回 值  : int    成功返回0,失败返回-1

*****************************************************************************/
int TCPlink(char *linkIP)
{
    // 套接字创建,SOCK_STREAM数据流会默认使用TCP协议
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0)
    {
        perror("socket TCP error\n");
        return -1;
    }
    printf("tcp socket is created\n");

    // ip端口设定,填写要连接的TCP设备信息
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr(linkIP);
    serv_addr.sin_port = htons(8989);
    printf("tcp connect start...\n");
    //connect()与bind()用法基本相同
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
    {
        perror("connect() failed");
        close(sock);
        return -1;
    }
    printf("tcp link succeed ip = %s\n", inet_ntoa(serv_addr.sin_addr));

    // 收发线程创建
    int *soc = &sock;
    pthread_t tidreceive, tidsend;
    if (pthread_create(&tidreceive, NULL, receive_thread2, (void *)soc) != 0)
    {
        perror("receive_thread2 create failed\n");
        close(sock);
        return -1;
    }
    if (pthread_create(&tidsend, NULL, send_thread2, (void *)soc) != 0)
    {
        perror("send_thread2 create failed\n");
        close(sock);
        return -1;
    }

    //收发线程回收
    if (pthread_join(tidreceive, NULL) != 0)
    {
        perror("recv_pthread_join error\n");
        close(sock);
        return -1;
    }
    if (pthread_join(tidsend, NULL) != 0)
    {
        perror("send_pthread_join error\n");
        close(sock);
        return -1;
    }

    close(sock);
    return 0;
}

3.2接收线程

/*****************************************************************************
 函数原型  : void *receive_thread2(void *arg)
 功能描述  : 进行TCP传输的网络数据接收与打印

 输入参数  : void *arg:    要接收的socket服务端的文件描述符
 输出参数  : 无
 返 回 值  : 无

*****************************************************************************/
void *receive_thread2(void *arg)
{
    int sock_fd = *(int *)arg;
    char recv_buf[1024];
    printf("tcp receive start\n");
    while (1)
    {
        memset(recv_buf, 0, sizeof(recv_buf));
        //recv函数与recvfrom类似,仅少后两位参数
        int recv_len = recv(sock_fd, recv_buf, sizeof(recv_buf), 0);
        if (recv_len < 0)
        {
            perror("recv failed\n");
            close(sock_fd);
            exit(EXIT_FAILURE);
        }
        else if (recv_len == 0)
        {
            printf("\nConnection closed by the server\n");
            break;
        }
        else
        {
            recv_buf[recv_len] = '\0'; // 确保字符串正确结束
            printf("\nReceived message: %s\n", recv_buf);
        }
    }
    pthread_exit(NULL);
}

3.3发送线程

/*****************************************************************************
 函数原型  : void *send_thread2(void *arg)
 功能描述  : 捕获键盘键入并发送

 输入参数  : void *arg:    要发送的socket服务端的文件描述符
 输出参数  : 无
 返 回 值  : 无
*****************************************************************************/
void *send_thread2(void *arg)
{
    int sock_fd = *(int *)arg;
    char send_buf[1024];
    printf("tcp send start\n");
    while (1)
    {
        memset(send_buf, 0, sizeof(send_buf));
        printf("Input message you will send: ");
        // 函数原型char *fgets(char *str, int num, FILE *stream);
        // 从标准输入(键盘键入)读取num-1个字符 +'/0'
        // eg:键盘输入888 +回车,str储存为 "888\n\0"
        fgets(send_buf, sizeof(send_buf), stdin);
        // send用法与recv类似
        int sent_bytes = send(sock_fd, send_buf, sizeof(send_buf), 0);
        if (sent_bytes < 0)
        {
            perror("send() failed\n");
            close(sock_fd);
            exit(EXIT_FAILURE);
        }
    }
    pthread_exit(NULL);
}

四、效果展示

4.1 Client端

        由于我目前我只在一台服务器上测试client端和server端,所以为了避免网卡和端口占用,测试展示过程中Client取消了bind操作,仅在server端进行了bind绑定

4.2 Server端

五、总结

        至此,S/C模型的Client部分就完成了,那么为什么要选用UDP来进行设备搜索呢?因为UDP不需要建立连接和确认(可类比TCP的三次握手),且能以点对多发送广播数据,使设备搜索变得简单,快速而高效。那为什么要选用TCP来进行数据通信呢?因为TCP 保证了数据包的完整可靠和有序交付,如果数据包丢失,TCP 会重新发送丢失的数据包,确保数据按顺序、完整无误地到达目的地。最后多线程的作用又是什么呢?当然是确保数据收发的时效性了,在你发消息的时候,我还要等你发完消息才能轮到我发嘛,或者轮到我的广播循环结束了才能收到你的消息嘛,那这程序也太烂了吧,哈哈ヾ(≧▽≦*)o

        最后再回顾一下Client端的实现流程,先用UDP广播进行局域网设备搜索,然后在得到正确回应的设备IP后结束UDP广播通信,转而进入TCP去请求连接,TCP连接成功后便可以进行S/C双向实时通信了。

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值