一、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双向实时通信了。