局域网内探测在线好友是如何做到的?

2024-08-29 11:27   重庆  

一、前言

随着局域网(LAN)应用的广泛使用,网络通信已经成为软件设计中不可或缺的一部分。局域网聊天软件作为一种常见的网络应用,可以实现多个用户之间的实时通信,广泛应用于企业内部沟通和小型网络环境中。本项目设计并实现一个基于C语言的局域网群聊程序,通过UDP广播搜索在线用户,并在发现其他在线应用程序后,自动建立TCP连接,实现消息的收发。本程序展示了如何在Windows环境下使用Winsock API进行网络编程,提供了对UDP和TCP协议的实际应用,体现了网络通信中的多线程处理、广播通信和实时消息传递的关键技术点。

二、好友探测功能

在局域网内探测并发现其他在线用户是局域网聊天软件最主要的核心功能。该过程涉及到局域网广播(UDP广播)和TCP连接两个关键步骤。

下面将详细介绍实现这一功能的方法和设计思路。

2.1 使用UDP广播探测在线用户

1.1 UDP广播的概念

UDP(用户数据报协议)是一种无连接的、轻量级的传输协议,适用于发送小数据包。UDP广播允许将数据包发送到局域网内的所有设备,所有在监听特定端口的设备都能够接收到广播消息。这种特性使得UDP广播非常适合用于探测和发现局域网内的在线设备。

1.2 探测思路

在程序启动时,客户端会通过UDP广播发送一个上线通知消息,表示自己已在线。其他监听同一端口的客户端接收到这一消息后,可以获知该客户端的IP地址,并识别出它在线。具体的实现步骤如下:

  1. 创建UDP套接字:为UDP通信创建一个套接字,并配置为允许广播。

  2. 发送广播消息:程序向局域网内的广播地址(通常为255.255.255.255)发送一个消息,例如"HELLO, I'M ONLINE"。这个消息会被局域网内所有监听相同端口的设备接收。

  3. 监听UDP消息:每个客户端都持续监听来自局域网内的UDP消息。一旦接收到广播消息,客户端会记录发送方的IP地址和端口,以确认该客户端在线。

2.2 建立TCP连接实现通信

2.1 TCP连接的必要性

UDP广播虽然可以有效地发现在线用户,但由于其无连接的特点,不适合用于长时间的可靠通信。因此,在发现其他在线用户后,程序需要通过TCP(传输控制协议)建立可靠的点对点连接。TCP是一种面向连接的协议,能够确保数据的完整性和顺序传输,非常适合用于聊天消息的传递。

2.2 连接建立的流程

  1. 接收广播后尝试连接:当客户端接收到来自其他用户的UDP广播后,会通过TCP连接到该用户。客户端会使用从UDP消息中获取的IP地址和预定义的TCP端口号,发起连接请求。

  2. 接受连接请求:已经在线的客户端会开启一个TCP监听套接字,等待来自其他客户端的连接请求。一旦有请求到达,程序将接受连接,并启动一个独立的线程处理与该客户端之间的消息通信。

  3. 消息收发:通过建立的TCP连接,用户可以实时发送和接收聊天消息,确保消息在网络不稳定的情况下仍能可靠传输。

2.3 多线程处理的必要性

由于UDP广播接收、TCP连接监听和消息收发等操作需要同时进行,程序采用了多线程的设计。每个功能模块都运行在独立的线程中,确保它们可以并行处理,互不干扰。这样不仅提高了程序的响应速度,还增强了用户体验,确保通信的实时性。

2.4 总结

通过UDP广播发现局域网内的在线用户,然后利用TCP协议建立可靠的通信连接,这是局域网聊天软件的核心设计思路。UDP广播的轻量和广泛性使得在线用户的探测变得高效,而TCP连接则保证了后续通信的可靠性。多线程的引入进一步优化了程序的性能,使得该局域网聊天软件在实际应用中表现出色。

三、代码实现

下面是完整的代码。在VS2022里运行测试。

cpp

#define _WINSOCK_DEPRECATED_NO_WARNINGS

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <process.h>

#pragma comment(lib, "ws2_32.lib")

#define UDP_PORT 8888
#define TCP_PORT 8889
#define BROADCAST_ADDR "255.255.255.255"
#define BUFFER_SIZE 1024

typedef struct ClientInfo {
SOCKET socket;
struct sockaddr_in address;
} ClientInfo;

void udp_broadcast_listener(void* param);
void tcp_connection_listener(void* param);
void tcp_message_listener(void* param);

int main() {
WSADATA wsaData;
SOCKET udp_socket, tcp_socket;
struct sockaddr_in udp_addr, tcp_addr;
char buffer[BUFFER_SIZE];
struct sockaddr_in client_addr;
int addr_len = sizeof(client_addr);

// 初始化 Winsock
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
printf("WSAStartup failed\n");
return 1;
}

// 创建 UDP 套接字
udp_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (udp_socket == INVALID_SOCKET) {
printf("UDP socket creation failed\n");
WSACleanup();
return 1;
}

// 配置 UDP 广播地址
memset(&udp_addr, 0, sizeof(udp_addr));
udp_addr.sin_family = AF_INET;
udp_addr.sin_port = htons(UDP_PORT);
udp_addr.sin_addr.s_addr = inet_addr(BROADCAST_ADDR);

// 启动 UDP 广播监听线程
_beginthread(udp_broadcast_listener, 0, NULL);

// 启动 TCP 连接监听线程
_beginthread(tcp_connection_listener, 0, NULL);

// 向局域网内广播自己上线
strcpy(buffer, "HELLO, I'M ONLINE");
sendto(udp_socket, buffer, strlen(buffer), 0, (struct sockaddr*)&udp_addr, sizeof(udp_addr));

while (1) {
fgets(buffer, BUFFER_SIZE, stdin);
buffer[strcspn(buffer, "\n")] = 0; // 移除换行符
sendto(udp_socket, buffer, strlen(buffer), 0, (struct sockaddr*)&udp_addr, sizeof(udp_addr));
}

closesocket(udp_socket);
WSACleanup();
return 0;
}

void udp_broadcast_listener(void* param) {
SOCKET udp_socket;
struct sockaddr_in udp_addr, sender_addr;
char buffer[BUFFER_SIZE];
int addr_len = sizeof(sender_addr);

// 创建 UDP 套接字
udp_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (udp_socket == INVALID_SOCKET) {
printf("UDP socket creation failed in listener\n");
return;
}

// 配置 UDP 地址
memset(&udp_addr, 0, sizeof(udp_addr));
udp_addr.sin_family = AF_INET;
udp_addr.sin_port = htons(UDP_PORT);
udp_addr.sin_addr.s_addr = INADDR_ANY;

// 绑定套接字
if (bind(udp_socket, (struct sockaddr*)&udp_addr, sizeof(udp_addr)) == SOCKET_ERROR) {
printf("UDP socket binding failed\n");
closesocket(udp_socket);
return;
}

while (1) {
int recv_len = recvfrom(udp_socket, buffer, BUFFER_SIZE, 0, (struct sockaddr*)&sender_addr, &addr_len);
if (recv_len > 0) {
buffer[recv_len] = '\0';
printf("Received UDP broadcast from %s: %s\n", inet_ntoa(sender_addr.sin_addr), buffer);

// 如果接收到"HELLO, I'M ONLINE",尝试建立TCP连接
if (strcmp(buffer, "HELLO, I'M ONLINE") == 0) {
SOCKET tcp_socket;
struct sockaddr_in tcp_addr;

tcp_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (tcp_socket == INVALID_SOCKET) {
printf("TCP socket creation failed\n");
continue;
}

// 配置 TCP 地址
memset(&tcp_addr, 0, sizeof(tcp_addr));
tcp_addr.sin_family = AF_INET;
tcp_addr.sin_port = htons(TCP_PORT);
tcp_addr.sin_addr.s_addr = sender_addr.sin_addr.s_addr;

if (connect(tcp_socket, (struct sockaddr*)&tcp_addr, sizeof(tcp_addr)) == SOCKET_ERROR) {
printf("TCP connection failed to %s\n", inet_ntoa(tcp_addr.sin_addr));
closesocket(tcp_socket);
}
else {
printf("Connected to %s\n", inet_ntoa(tcp_addr.sin_addr));

// 启动 TCP 消息监听线程
ClientInfo* client = (ClientInfo*)malloc(sizeof(ClientInfo));
client->socket = tcp_socket;
client->address = tcp_addr;
_beginthread(tcp_message_listener, 0, client);
}
}
}
}

closesocket(udp_socket);
}

void tcp_connection_listener(void* param) {
SOCKET tcp_socket, client_socket;
struct sockaddr_in tcp_addr, client_addr;
int addr_len = sizeof(client_addr);

// 创建 TCP 套接字
tcp_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (tcp_socket == INVALID_SOCKET) {
printf("TCP socket creation failed\n");
return;
}

// 配置 TCP 地址
memset(&tcp_addr, 0, sizeof(tcp_addr));
tcp_addr.sin_family = AF_INET;
tcp_addr.sin_port = htons(TCP_PORT);
tcp_addr.sin_addr.s_addr = INADDR_ANY;

// 绑定套接字
if (bind(tcp_socket, (struct sockaddr*)&tcp_addr, sizeof(tcp_addr)) == SOCKET_ERROR) {
printf("TCP socket binding failed\n");
closesocket(tcp_socket);
return;
}

// 开始监听
if (listen(tcp_socket, 5) == SOCKET_ERROR) {
printf("TCP socket listen failed\n");
closesocket(tcp_socket);
return;
}

printf("TCP connection listener started...\n");

while (1) {
client_socket = accept(tcp_socket, (struct sockaddr*)&client_addr, &addr_len);
if (client_socket == INVALID_SOCKET) {
printf("TCP accept failed\n");
continue;
}

printf("Accepted connection from %s\n", inet_ntoa(client_addr.sin_addr));

// 启动 TCP 消息监听线程
ClientInfo* client = (ClientInfo*)malloc(sizeof(ClientInfo));
client->socket = client_socket;
client->address = client_addr;
_beginthread(tcp_message_listener, 0, client);
}

closesocket(tcp_socket);
}

void tcp_message_listener(void* param) {
ClientInfo* client = (ClientInfo*)param;
char buffer[BUFFER_SIZE];
int recv_len;

while ((recv_len = recv(client->socket, buffer, BUFFER_SIZE, 0)) > 0) {
buffer[recv_len] = '\0';
printf("Message from %s: %s\n", inet_ntoa(client->address.sin_addr), buffer);
}

printf("Connection closed by %s\n", inet_ntoa(client->address.sin_addr));
closesocket(client->socket);
free(client);
}

程序在主函数里通过 WSAStartup 函数初始化Winsock库,这是一种Windows平台上的网络编程库,提供了网络通信所需的API。初始化成功后,程序可以使用Winsock提供的各种网络功能。

创建了两个主要的套接字:

  • UDP套接字:用于广播消息和接收其他设备的广播。

  • TCP套接字:用于建立点对点的通信连接。

在程序启动时,通过UDP广播向局域网内所有设备发送一个“HELLO, I'M ONLINE”的消息。这一消息用来告知局域网内的其他用户自己的存在,从而实现在线用户的探测。

为了接收其他用户的广播消息,程序创建了一个UDP套接字并绑定到特定的端口上(UDP_PORT)。程序通过这个套接字监听局域网内的所有广播消息,提取发送者的IP地址,并处理接收到的消息。

一旦接收到来自其他在线用户的UDP广播消息,程序会尝试通过TCP建立连接。步骤包括:

  • 从UDP消息中提取发送者的IP地址。

  • 使用提取的IP地址和预定义的TCP端口号发起TCP连接请求。

  • 如果连接成功,程序将建立一个可靠的点对点通信通道,用于后续的聊天消息传递。

程序创建一个TCP套接字,并在特定端口上进行监听,等待其他用户的连接请求。当有新的连接请求到达时,程序接受该连接,并为每个连接创建一个新的线程,以处理与该连接相关的消息通信。

用户在键盘上输入消息后,程序通过UDP套接字广播该消息到局域网内的所有在线用户。此功能确保所有在线的用户都能看到发送的消息。

程序通过TCP连接接收来自其他用户的消息。接收到的消息将被显示在终端上,提供实时的聊天功能。每个TCP连接使用一个独立的线程进行处理,确保消息的及时传递和处理。

程序采用了多线程技术来并行处理不同的任务,确保系统的响应性和效率。主要线程包括:

  • UDP广播监听线程:处理UDP广播消息的接收和处理。

  • TCP连接监听线程:接受来自其他用户的TCP连接请求。

  • TCP消息处理线程:处理与每个已连接用户之间的消息交换。

在程序退出时,所有打开的套接字都会被关闭,资源得到释放。程序通过调用 closesocket 函数关闭套接字,并调用 WSACleanup 进行Winsock库的清理,确保程序在退出时不会泄漏资源。


DS小龙哥 嵌入式技术资讯
不定时更新STM32、物联网、linux驱动、QT等技术文章;打造嵌入式开发相关知识分享、技术交流平台
 最新文章