- 实现需求
- TCP 心跳机制解决Soket异常断开问题
[二、Linux内核提供了通过sysctl命令查看和配置TCP KeepAlive参数的方法。](#二、Linux内核提供了通过sysctl命令查看和配置TCP KeepAlive参数的方法。)
- 查看当前系统的TCP KeepAlive参数
- 修改TCP KeepAlive参数
[三、C语言实现TCP KeepAlive功能](#三、C语言实现TCP KeepAlive功能)
四、setsockopt用于设置套接字选项的系统调用
五、代码实现
六、待定
一、增加网络控制功能
Linux网络编程(TCP Socket编程实现过程)
TCP Keepalive HOWTO
实现需求:
Socket网络编程,通过Socket远程接入控制垃圾识别功能。
TCP 心跳机制解决Soket异常断开问题
Socket客户端得断开情形无非就两种情况:
- 客户端能够发送状态给服务器;正常断开,强制关闭客户端等,客户端能够做出反应。
- 客户端不能发送状态给服务器;突然断网,断电,客户端卡死等,客户端根本没机会做出反应,服务器更不了解客户端状态,导致服务器异常等待。
为了解决上述问题,引入TCP心跳包机制:
心跳包的实现,心跳包就是服务器定时向客户端发送查询信息,如果客户端有回应就代表连接正常,类似于linux系统的看门狗机制。心跳包的机制有一种方法就是采用TCP_KEEPALIVE机制,它是一种用于检测TCP连接是否存活的机制,它的原理是在一定时间内没有数据往来时,发送探测包给对方,如果对方没有响应,就认为连接已经断开。TCP_KEEPALIVE机制可以通过设置一些参数来调整,如探测时间间隔、探测次数等。
二、Linux内核提供了通过sysctl命令查看和配置TCP KeepAlive参数的方法。
在 Linux 内核中,可以通过 sysctl
命令来查看和配置 TCP KeepAlive 参数。TCP KeepAlive 是一种机制,用于在两个端点之间的连接空闲时检测连接的活跃性。
以下是一些与 TCP KeepAlive 相关的 sysctl 参数以及它们的含义:
- tcp_keepalive_time:
- 含义:TCP KeepAlive 的开始时间。表示连接空闲的时间,超过这个时间,内核开始发送 KeepAlive 消息。
- 默认值:7200 秒(2 小时)
bash
sysctl net.ipv4.tcp_keepalive_time
- tcp_keepalive_intvl:
- 含义:两次 KeepAlive 消息之间的时间间隔。
- 默认值:75 秒
bash
sysctl net.ipv4.tcp_keepalive_intvl
- tcp_keepalive_probes:
- 含义:在放弃连接之前发送的 KeepAlive 消息的次数。
- 默认值:9 次
bash
sysctl net.ipv4.tcp_keepalive_probes
这些参数的配置可以通过修改 /etc/sysctl.conf
文件来实现。例如,要将 tcp_keepalive_time
设置为 600 秒,可以在 /etc/sysctl.conf
中添加如下行:
bash
net.ipv4.tcp_keepalive_time = 600
然后,可以运行以下命令使更改生效:
bash
sysctl -p
请注意,修改这些参数可能会对系统的整体性能产生影响,因此在进行更改之前,请仔细了解每个参数的含义以及它们的默认值。
查看当前系统的TCP KeepAlive参数
bash
sysctl net.ipv4.tcp_keepalive_time
sysctl net.ipv4.tcp_keepalive_probes
sysctl net.ipv4.tcp_keepalive_intvl
修改TCP KeepAlive参数
bash
sysctl net.ipv4.tcp_keepalive_time=3600
三、C语言实现TCP KeepAlive功能
对于Socket而言,可以在程序中通过socket选项开启TCP KeepAlive功能,并配置参数。
对应的Socket选项分别为 SO_KEEPALIVE 、TCP_KEEPIDLE 、 TCP_KEEPCNT 、 TCP_KEEPINTVL 。
关于 setsockopt
函数的参数和相应的套接字选项以及协议的对应关系,以及对应的描述。
setsockopt 参数 | 描述 | 对应的 Socket Level(SOL) | 对应的协议(Protocol) |
---|---|---|---|
SO_KEEPALIVE | 启用或禁用在套接字上发送保持活动消息。保持活动消息用于检测连接是否仍然活动。 | SOL_SOCKET | IPPROTO_TCP |
TCP_KEEPIDLE | 指定连接在 TCP 开始发送保持活动探测之前必须空闲的时间(以秒为单位)。 | SOL_TCP | IPPROTO_TCP |
TCP_KEEPCNT | 指定在将连接视为中断之前 TCP 应发送的最大保持活动探测次数。 | SOL_TCP | IPPROTO_TCP |
TCP_KEEPINTVL | 指定个别保持活动探测之间的时间(以秒为单位)。 | SOL_TCP | IPPROTO_TCP |
IPPROTO_TCP | TCP(传输控制协议)的协议级别。用于设置特定于 TCP 的套接字选项。 | N/A | IPPROTO_TCP |
setsockopt
中参数与套接字选项和协议的关系。
Socket 选项和 TCP 参数
选项/参数 | 描述 |
---|---|
SO_KEEPALIVE |
启用或禁用在套接字上发送保持活动消息。保持活动消息用于检测连接是否仍然活动。 |
TCP_KEEPIDLE |
指定连接在 TCP 开始发送保持活动探测之前必须空闲的时间(以秒为单位)。 |
TCP_KEEPCNT |
指定在将连接视为中断之前 TCP 应发送的最大保持活动探测次数。 |
TCP_KEEPINTVL |
指定个别保持活动探测之间的时间(以秒为单位)。 |
四、setsockopt用于设置套接字选项的系统调用
setsockopt
函数是用于设置套接字选项的系统调用。下面是 setsockopt
函数的基本格式:
c
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
其中:
sockfd
:套接字文件描述符。level
:选项所在的协议层。常用的有SOL_SOCKET
(用于通用套接字选项)和特定的协议层如IPPROTO_TCP
(用于 TCP 协议选项)。optname
:选项名称,用于指定需要设置的具体选项。optval
:指向包含选项值的缓冲区的指针。optlen
:指定optval
缓冲区的大小。
下面是一些常见的套接字选项和它们的作用:
-
SO_KEEPALIVE
(SOL_SOCKET)- 描述:启用或禁用在套接字上发送保持活动消息。保持活动消息用于检测连接是否仍然活动。
-
TCP_KEEPIDLE
(SOL_TCP)- 描述:指定连接在 TCP 开始发送保持活动探测之前必须空闲的时间(以秒为单位)。
-
TCP_KEEPCNT
(SOL_TCP)- 描述:指定在将连接视为中断之前 TCP 应发送的最大保持活动探测次数。
-
TCP_KEEPINTVL
(SOL_TCP)- 描述:指定个别保持活动探测之间的时间(以秒为单位)。
-
IPPROTO_TCP
- 描述:TCP(传输控制协议)的协议级别。用于设置特定于 TCP 的套接字选项。
这些选项可用于控制套接字的行为,如保持连接活动、调整发送和接收缓冲区的大小等。在使用时,请注意确保提供正确的协议级别和选项名称。
c
int keepalive = 1; // 开启TCP KeepAlive功能
int keepidle = 5; // tcp_keepalive_time 3s内没收到数据开始发送心跳包
int keepcnt = 3; // tcp_keepalive_probes 每次发送心跳包的时间间隔,单位秒
int keepintvl = 3; // tcp_keepalive_intvl 每3s发送一次心跳包
setsockopt(client_socketfd, SOL_SOCKET, SO_KEEPALIVE, (void *)&keepalive, sizeof(keepalive));
setsockopt(client_socketfd, SOL_TCP, TCP_KEEPIDLE, (void *)&keepidle, sizeof(keepidle));
setsockopt(client_socketfd, SOL_TCP, TCP_KEEPCNT, (void *)&keepcnt, sizeof(keepcnt));
setsockopt(client_socketfd, SOL_TCP, TCP_KEEPINTVL, (void *)&keepintvl, sizeof(keepintvl));
五、代码实现
mysocket.h
c
#ifndef __MYSOCKET__H
#define __MYSOCKET__H
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netinet/tcp.h>
#include <errno.h>
#include <unistd.h>
#define IPADDR "192.168.1.254" //填写自己实际的ip地址
#define IPPORT "8192"
#define BUF_SIZE 6
int socket_init(const char *ipaddr, const char *port);
#endif
mysocket.c
c
#include "mysocket.h"
int socket_init(const char *ipaddr, const char *port)
{
int server_fd = -1;
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(struct sockaddr_in));
// 创建服务器套接字
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
perror("Socket creation failed");
return -1;
}
// 设置服务器地址结构
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(port);
// server_addr.sin_addr.s_addr = INADDR_ANY;
inet_aton(ipaddr, &server_addr.sin_addr);
// 绑定服务器套接字
if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("Binding failed");
return -1;
}
// 监听传入的连接请求
if (listen(server_fd, 1) == 0) { //只监听1个连接,排队扔垃圾
printf("Listening for incoming connections...\n");
} else {
perror("Listening failed");
return -1;
}
return server_fd;
}
main.c
c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <wiringPi.h>
#include <pthread.h>
#include "uartTool.h"
#include "garbage.h"
#include "pwm.h"
#include "myoled.h"
#include "mysocket.h"
int serial_fd = -1; // 串口文件描述符
pthread_cond_t cond; // 条件变量,用于线程之间的条件同步
pthread_mutex_t mutex; // 互斥锁,用于线程之间的互斥访问
// 判断进程是否在运行
static int detect_process(const char * process_name)
{
int n = -1; // 存储进程PID,默认为-1
FILE *strm;
char buf[128] = {0}; // 缓冲区
// 构造命令字符串,通过ps命令查找进程
sprintf(buf, "ps -ax | grep %s|grep -v grep", process_name);
// 使用popen执行命令并读取输出
if ((strm = popen(buf, "r")) != NULL) {
if (fgets(buf, sizeof(buf), strm) != NULL) {
printf("buf = %s\n", buf); //打印缓存区的内容
n = atoi(buf); // 将进程ID字符串转换为整数
printf("n = %d\n", n); // 打印下进程的PID
}
}
else {
return -1; // popen失败
}
pclose(strm); // 关闭popen打开的文件流
return n;
}
// 获取语音线程
void *pget_voice(void *arg)
{
unsigned char buffer[6] = {0xAA, 0x55, 0x00, 0x00, 0X55, 0xAA};
int len = 0;
printf("%s|%s|%d\n", __FILE__, __func__, __LINE__);
// 串口未打开,退出线程
if (-1 == serial_fd) {
printf("%s|%s|%d: open serial failed\n", __FILE__, __func__, __LINE__);
pthread_exit(0);
}
printf("%s|%s|%d\n", __FILE__, __func__, __LINE__);
// 循环读取串口数据
while (1) {
len = my_serialGetstring(serial_fd, buffer);
printf("%s|%s|%d, len = %d\n", __FILE__, __func__, __LINE__, len);
// 检测到特定数据,发出信号唤醒其他线程
if (len > 0 && buffer[2] == 0x46) {
printf("%s|%s|%d\n", __FILE__, __func__, __LINE__);
pthread_mutex_lock(&mutex);
buffer[2] = 0x00;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
system(WGET_CMD);
}
}
pthread_exit(0);
}
// 发送语音线程
void *psend_voice(void *arg)
{
pthread_detach(pthread_self());
unsigned char *buffer = (unsigned char *)arg;
// 串口未打开,退出线程
if (-1 == serial_fd) {
printf("%s|%s|%d: open serial failed\n", __FILE__, __func__, __LINE__);
pthread_exit(0);
}
// buffer不为空时,通过串口发送数据(分类结果)
if (NULL != buffer) {
my_serialSendstring(serial_fd, buffer, 6);
}
pthread_exit(0);
}
// 控制垃圾桶线程
void *popen_trash_can(void *arg)
{
pthread_detach(pthread_self());
unsigned char *buffer = (unsigned char *)arg;
// 根据垃圾类型控制PWM
if (buffer[2] == 0x43) { // 可回收垃圾
printf("%s|%s|%d: buffer[2] = 0x%x\n", __FILE__, __func__, __LINE__, buffer[2]);
pwm_write(PWM_RECOVERABLE_GARBAGE);
delay(2000);
pwm_stop(PWM_RECOVERABLE_GARBAGE);
}
else if (buffer[2] == 0x41) { // 干垃圾
printf("%s|%s|%d: buffer[2] = 0x%x\n", __FILE__, __func__, __LINE__, buffer[2]);
pwm_write(PWM_GARBAGE);
delay(2000);
pwm_stop(PWM_GARBAGE);
}
else if (buffer[2] == 0x42) { // 湿垃圾
printf("%s|%s|%d: buffer[2]=0x%x\n", __FILE__, __func__, __LINE__,buffer[2]);
pwm_write(PWM_WET_GARBAGE);
delay(2000);
pwm_stop(PWM_WET_GARBAGE);
}
else if (buffer[2] == 0x44) { // 有害垃圾
printf("%s|%s|%d: buffer[2]=0x%x\n", __FILE__, __func__, __LINE__,buffer[2]);
pwm_stop(PWM_HAZARDOUS_GARBAGE);
delay(2000);
pwm_write(PWM_HAZARDOUS_GARBAGE);
}
pthread_exit(0);
}
// 在线程中显示 OLED
void *poled_show(void *arg)
{
// 分离线程,使其在退出时能够自动释放资源
pthread_detach(pthread_self());
// 初始化 OLED
myoled_init();
// 在 OLED 上显示垃圾分类结果
oled_show(arg);
// 退出线程
pthread_exit(0);
}
// 垃圾分类线程
void *pcategory(void *arg)
{
unsigned char buffer[6] = {0xAA, 0x55, 0x00, 0x00, 0X55, 0xAA};
char *category = NULL;
pthread_t send_voice_tid, trash_tid, oled_tid;
while (1) {
printf("%s|%s|%d: \n", __FILE__, __func__, __LINE__);
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond, &mutex);
pthread_mutex_unlock(&mutex);
printf("%s|%s|%d: \n", __FILE__, __func__, __LINE__);
buffer[2] = 0x00;
// 在执行wget命令之前添加调试输出
printf("Executing wget command...\n");
// 使用系统命令拍照
system(WGET_CMD);
// 在执行wget命令之后添加调试输出
printf("Wget command executed.\n");
// 判断垃圾种类
if (0 == access(GARBAGE_FILE, F_OK)) {
category = garbage_category(category);
if (strstr(category, "干垃圾")) {
buffer[2] = 0x41;
}
else if (strstr(category, "湿垃圾")) {
buffer[2] = 0x42;
}
else if (strstr(category, "可回收垃圾")) {
buffer[2] = 0x43;
}
else if (strstr(category, "有害垃圾")) {
buffer[2] = 0x44;
}
else {
buffer[2] = 0x45; // 未识别到垃圾类型
}
}
else {
buffer[2] = 0x45; // 识别失败
}
// 开垃圾桶开关
pthread_create(&trash_tid, NULL, psend_voice, (void *)buffer);
// 开语音播报线程
pthread_create(&send_voice_tid, NULL, popen_trash_can, (void *)buffer);
//oled显示线程
pthread_create(&oled_tid, NULL, poled_show, (void *)buffer);
// buffer[2] = 0x00;
// 删除拍照文件
remove(GARBAGE_FILE);
}
pthread_exit(0);
}
void *pget_socket(void *arg)
{
int server_fd = -1; // 服务器 socket 文件描述符
int client_fd = -1; // 客户端 socket 文件描述符
char buffer[6]; // 用于存储接收到的数据的缓冲区
int nread = -1; // 接收到的数据长度
struct sockaddr_in client_addr; // 客户端地址信息结构体
int clen = sizeof(struct sockaddr_in);
memset(&client_addr, 0, sizeof(struct sockaddr_in));
// 初始化服务器 socket
server_fd = socket_init(IPADDR, IPPORT);
printf("%s|%s|%d:server_fd = %d\n", __FILE__, __func__, __LINE__, server_fd);
if (-1 == server_fd) {
pthread_exit(0); // 初始化失败,退出线程
}
sleep(3); // 等待 socket 初始化完成
while (1) {
// 接受客户端连接
client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &clen);
// 配置 TCP KeepAlive 参数
int keepalive = 1; // 开启TCP KeepAlive功能
int keepidle = 5; // tcp_keepalive_time 3s内没收到数据开始发送心跳包
int keepcnt = 3; // tcp_keepalive_probes 每次发送心跳包的时间间隔,单位秒
int keepintvl = 3; // tcp_keepalive_intvl 每3s发送一次心跳包
setsockopt(client_fd, SOL_SOCKET, SO_KEEPALIVE, (void *)&keepalive, sizeof(keepalive));
setsockopt(client_fd, SOL_TCP, TCP_KEEPIDLE, (void *)&keepidle, sizeof(keepidle));
setsockopt(client_fd, SOL_TCP, TCP_KEEPCNT, (void *)&keepcnt, sizeof(keepcnt));
setsockopt(client_fd, SOL_TCP, TCP_KEEPINTVL, (void *)&keepintvl, sizeof(keepintvl));
printf("%s|%s|%d: Accept a connection from %s:%d\n", __FILE__, __func__, __LINE__,
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
if (client_fd == -1) {
perror("accept");
continue; // 如果接受连接失败,继续下一次循环
}
while (1)
{
memset(buffer, 0, sizeof(buffer));
// 从客户端接收数据
nread = recv(client_fd, buffer, sizeof(buffer), 0); //n_read = read(c_fd, buffer, sizeof(buffer));
printf("%s|%s|%d:nread = %d, buffer = %s\n", __FILE__, __func__, __LINE__, nread, buffer);
// 根据接收到的数据执行相应的操作
if (nread > 0)
{
if (strstr(buffer, "open")) {
pthread_mutex_lock(&mutex);
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
}
if (strstr(buffer, "k1")) {
pwm_write(PWM_RECOVERABLE_GARBAGE);
delay(2000);
pwm_stop(PWM_RECOVERABLE_GARBAGE);
}
if (strstr(buffer, "k2")) {
pwm_write(PWM_GARBAGE);
delay(2000);
pwm_stop(PWM_GARBAGE);
}
if (strstr(buffer, "k3")) {
pwm_write(PWM_WET_GARBAGE);
delay(2000);
pwm_stop(PWM_WET_GARBAGE);
}
if (strstr(buffer, "k4")) {
pwm_stop(PWM_HAZARDOUS_GARBAGE);
delay(2000);
pwm_write(PWM_HAZARDOUS_GARBAGE);
}
}
else if (0 == nread || -1 == nread) {
break; // 如果接收到的数据长度为 0 或者出错,跳出循环
}
}
close(client_fd); // 关闭客户端连接
}
pthread_exit(0); // 退出线程
}
int main(int argc, char *argv[])
{
int ret = -1;
int len = 0;
char *category = NULL;
pthread_t get_voice_tid, category_tid, get_socket_tid;
wiringPiSetup();
// 初始化串口和垃圾分类模块
garbage_init ();
// 用于判断mjpg_streamer服务是否已经启动
ret = detect_process ("mjpg_streamer");
if (-1 == ret) {
printf("detect process failed\n");
goto END;
}
// 打开串口
serial_fd = my_serialOpen (SERIAL_DEV, BAUD);
if (-1 == serial_fd) {
printf("open serial failed\n");
goto END;
}
// 开语音线程
printf("%s|%s|%d\n", __FILE__, __func__, __LINE__);
pthread_create(&get_voice_tid, NULL, pget_voice, NULL);
// 开网络线程
printf("%s|%s|%d\n", __FILE__, __func__, __LINE__);
pthread_create(&get_socket_tid, NULL, pget_socket, NULL);
// 开阿里云交互线程
printf("%s|%s|%d\n", __FILE__, __func__, __LINE__);
pthread_create(&category_tid, NULL, pcategory, NULL);
// 创建互斥锁和条件变量
pthread_join(get_voice_tid, NULL);
pthread_join(category_tid, NULL);
pthread_join(get_socket_tid, NULL);
// 销毁互斥锁和条件变量
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
// 关闭串口
close(serial_fd);
END:
// 释放垃圾分类资源
garbage_final();
return 0;
}