2024-08-16

Modbus是一种工业通信协议,常用于工业自动化设备之间的数据通信。Modbus TCP、RTU和ASCII是Modbus的三种传输方式,其中TCP是基于以太网的传输方式,RTU和ASCII是基于串行链路的传输方式。

在开发基于Modbus协议的应用时,通常需要一个Modbus协议栈或者中间件来处理不同设备之间的通信。以下是一个简单的Python示例,使用pymodbus库来实现Modbus TCP服务器。

首先,需要安装pymodbus库:




pip install pymodbus

然后,可以使用以下代码创建一个简单的Modbus TCP服务器:




from pymodbus.server.sync import StartTcpServer
from pymodbus.datastore import ModbusSequentialDataBlock, ModbusSlaveContext, ModbusServerContext
from pymodbus.transaction import ModbusRtuFramer, ModbusBinaryFramer
 
# 设置从机上的寄存器和线圈数量
data_block = ModbusSequentialDataBlock(0, [17]*10 + [3]*10 + [65535]*10)
 
# 设置数据块从机上下文
context = ModbusSlaveContext(
    di=data_block,
    co=data_block,
    hr=data_block,
    ir=data_block
)
 
# 创建服务器上下文
server = ModbusServerContext(context)
 
# 设置服务器和端口
address = ("localhost", 502)
 
# 创建TCP服务器
start_server = StartTcpServer(server, address=address)
 
# 启动服务器
start_server.start()

这段代码创建了一个Modbus TCP服务器,它有10个寄存器,10个线圈,10个保持寄存器。它将所有的输入、输出、保持寄存器和线圈值初始化为示例数据。

注意:在实际应用中,你需要根据自己的网络环境和设备配置相应地调整IP地址和端口号,并且配置数据块的大小和类型。

2024-08-14

以下是一个使用modbus4j实现Modbus TCP客户端的简单示例代码。请注意,在运行此代码之前,您需要有一个Modbus TCP服务器运行在网络上的某个地址。




import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
 
import org.apache.log4j.BasicConfigurator;
 
import com.intelligt.modbus.jlibmodbus.Master;
import com.intelligt.modbus.jlibmodbus.exception.ModbusInitException;
import com.intelligt.modbus.jlibmodbus.exception.ModbusTransportException;
 
public class ModbusTcpClientExample {
    public static void main(String[] args) {
        BasicConfigurator.configure();
 
        try {
            ExecutorService executor = Executors.newSingleThreadExecutor();
            Master master = Master.createTcpMaster("127.0.0.1", 502, true, "127.0.0.1", 3);
            executor.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        master.init();
                        short[] registers = master.readHoldingRegisters(0, 10, 0);
                        for (short register : registers) {
                            System.out.println(register);
                        }
                    } catch (ModbusTransportException e) {
                        e.printStackTrace();
                    } finally {
                        try {
                            master.destroy();
                        } catch (ModbusInitException e) {
                            e.printStackTrace();
                        }
                    }
                }
            });
 
            executor.shutdown();
            executor.awaitTermination(10, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            e.printStackTrace();
        }
    }
}

在这个示例中,我们首先配置了日志记录。然后,我们创建了一个Modbus TCP客户端,指定了服务器的IP地址和端口号。在单独的线程中,我们初始化了Master实例,并读取了从地址0开始的10个保持寄存器的数值。读取到的寄存器值将被打印输出。最后,我们在完成操作后销毁了Master实例,并关闭了执行器服务。

请注意,上述代码中的IP地址和端口号以及从机地址都应该根据您的实际Modbus TCP服务器进行相应的修改。

2024-08-14

在TCP中,快重传(SACK)和滑动窗口(TCP流量控制)是两种用于改善网络传输效率的机制。

快重传是一种SACK(选择性确认)的机制,它允许接收方通过单个重复ACK向发送方通知丢失的数据段,而不是等待超时。这样可以使得发送方尽快重传丢失的数据,而不是等待整个重传算法的时间周期。

以下是快重传的示例代码(假设已有TCP包处理逻辑):




// 假设tcp_recv是TCP接收数据的函数
void tcp_recv(struct tcp_packet *packet) {
    // ... 接收数据处理逻辑 ...
 
    // 检测数据丢失
    if (数据丢失) {
        // 通知快重传
        tcp_send_dup_ack(packet->seq_num);
        // 标记快重传
        tcp_set_sack_flag();
        // 重传丢失的数据
        tcp_retransmit_lost_data(packet->seq_num);
    }
}

滑动窗口是TCP用于流量控制的机制,它通过调整发送方的发送速率来避免网络拥塞。

以下是设置TCP滑动窗口大小的示例代码:




// 设置TCP滑动窗口大小
void tcp_set_window_size(int new_window_size) {
    // 确保窗口大小不超过TCP协议允许的最大值
    if (new_window_size > TCP_MAX_WINDOW_SIZE) {
        new_window_size = TCP_MAX_WINDOW_SIZE;
    }
    // 设置窗口大小
    tcp_window_size = new_window_size;
}
 
// 更新滑动窗口
void tcp_update_window(struct tcp_packet *packet) {
    // 根据接收情况调整窗口大小
    tcp_set_window_size(当前窗口大小 + packet->win_size_increment);
}

快重传和滑动窗口是TCP协议的重要组成部分,用于提高网络传输的效率和稳定性。在实际编程中,这些机制通常由操作系统的TCP/IP栈实现,开发者通常不需要手动处理它们。但了解它们的工作原理和效果可以帮助开发者更好地理解网络编程。

2024-08-14

在Linux中,TCP的块模式和非阻塞模式是两种常见的I/O模型。

  1. 阻塞I/O(blocking I/O):默认情况下,所有的套接字都是阻塞的。当进程调用一个阻塞的I/O函数时,该进程会被挂起,直到有数据可供处理。
  2. 非阻塞I/O(nonblocking I/O):通过设置套接字选项为非阻塞,进程可以直接调用recvfrom()等函数,如果没有数据可读,这些函数会立即返回一个EWOULDBLOCK错误,而不会挂起进程。
  3. TCP字节流(TCP stream):TCP作为一种字节流协议,提供了一种可靠的、面向连接的数据传输服务。
  4. TCP异常(TCP exceptions):TCP异常指的是TCP协议中的一些特殊情况,如连接断开、网络超时等。

对于TCP异常的处理,可以使用select()或poll()系统调用,它们可以等待多个文件描述符上的某种事件,如果任何一个文件描述符上的事件发生,select()或poll()就会返回。这样,你可以检查哪个socket或文件描述符可以进行无阻塞的I/O操作。

以下是一个使用select()处理TCP异常的简单示例:




#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
 
int main() {
    fd_set rfds;
    struct timeval tv;
    int retval;
 
    // 清除文件描述符集合
    FD_ZERO(&rfds);
 
    // 添加你想要检查的文件描述符到集合中
    FD_SET(0, &rfds); // 标准输入
    FD_SET(sockfd, &rfds); // 你的socket文件描述符
 
    // 超时设置
    tv.tv_sec = 1; // 秒
    tv.tv_usec = 0; // 微秒
 
    // 调用select()
    retval = select(sockfd + 1, &rfds, NULL, NULL, &tv);
 
    if (retval == -1) {
        // 错误处理
        perror("select()");
        exit(1);
    } else if (retval) {
        // 如果retval非零,则至少有一个描述符的事件发生了
        if (FD_ISSET(sockfd, &rfds)) {
            // 你的socket文件描述符上的事件
            // 可能是可读、可写或异常
            // 对于异常,你可能需要调用getsockopt()来检查
            // SOCKET_ERROR来获取错误代码
        }
    } else {
        // 超时处理
        printf("select() timed out.\n");
    }
 
    return 0;
}

在这个例子中,select()会等待数据在标准输入或者指定的socket上可读、可写或者发生异常。如果在指定时间内没有任何事件发生,select()会超时返回。如果发生异常,你可能需要通过getsockopt()函数和SO\_ERROR选项来检查具体的错误代码。

2024-08-14

在Linux环境下,可以使用nc(netcat)命令来发送和接收UDP和TCP数据。

UDP数据的发送和接收

发送UDP数据:




echo "Hello, World!" | nc -u -w 1 127.0.0.1 12345

在另一个终端接收UDP数据:




nc -ul 12345

TCP数据的发送和接收

发送TCP数据:




echo "Hello, World!" | nc 127.0.0.1 12345

在另一个终端接收TCP数据:




nc -l 12345

这里,-u 选项用于UDP,没有 -u 是TCP。-w 选项用于设置超时时间,127.0.0.1 是本地回环地址,12345 是端口号。

请确保目标机器上的端口号没有被其他服务占用,并且你有权限在这些端口上进行通信。

2024-08-13

在Linux系统中,你可以使用find命令来查找和删除7天前的文件。以下是两种常见的方法:

  1. 使用find命令查找并删除7天前的文件:



find /path/to/directory -type f -mtime +6 -exec rm -f {} \;

解释:

  • /path/to/directory 是你想要搜索文件的目录路径。
  • -type f 表示搜索文件。
  • -mtime +6 表示修改时间超过6天的文件。
  • -exec rm -f {} \; 对符合条件的文件执行删除操作。
  1. 使用find命令结合xargs来删除文件:



find /path/to/directory -type f -mtime +6 | xargs rm -f

解释:

  • 这种方法使用xargs来避免在有大量文件要删除的情况下可能出现的命令行参数限制问题。

请根据你的具体需求选择合适的方法,并确保你有足够的权限来删除目标目录下的文件。在执行删除操作之前,请务必检查find命令的输出,以确保不会误删重要文件。

2024-08-13

以下是针对Go语言网络编程的TCP和UDP的简单示例代码。

TCP服务器端:




package main
 
import (
    "fmt"
    "net"
)
 
func main() {
    listener, err := net.Listen("tcp", "localhost:50000")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer listener.Close()
 
    fmt.Println("Listening on localhost:50000...")
    for {
        conn, err := listener.Accept()
        if err != nil {
            fmt.Println(err)
            continue
        }
        go handleTCPConnection(conn)
    }
}
 
func handleTCPConnection(conn net.Conn) {
    defer conn.Close()
    fmt.Fprintf(conn, "Hello, client! Type 'exit' to quit.\n")
    for {
        buf := make([]byte, 1024)
        n, err := conn.Read(buf)
        if err != nil {
            fmt.Println(err)
            return
        }
        if string(buf[:n-1]) == "exit" {
            fmt.Println("Client exited.")
            return
        }
        fmt.Fprintf(conn, "Received: %s\n", string(buf[:n-1]))
    }
}

TCP客户端:




package main
 
import (
    "bufio"
    "fmt"
    "net"
    "os"
    "strings"
)
 
func main() {
    conn, err := net.Dial("tcp", "localhost:50000")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer conn.Close()
 
    go func() {
        reader := bufio.NewReader(os.Stdin)
        for {
            data, err := reader.ReadString('\n')
            if err != nil {
                fmt.Println(err)
                return
            }
            trimmedData := strings.TrimSpace(data)
            if trimmedData == "exit" {
                conn.Close()
                return
            }
            conn.Write([]byte(trimmedData))
        }
    }()
 
    for {
        buf := make([]byte, 1024)
        n, err := conn.Read(buf)
        if err != nil {
            fmt.Println(err)
            return
        }
        fmt.Printf("Server: %s\n", string(buf[:n]))
    }
}

UDP服务器端:




package main
 
import (
    "fmt"
    "net"
)
 
func main() {
    addr, err := net.ResolveUDPAddr("udp", "localhost:50001")
    if err != nil {
        fmt.Println(err)
        return
    }
 
    conn, err := net.ListenUDP("udp", addr)
    if err != nil {
        fmt.Println(err)
        return
    }
    defer conn.Close()
 
    fmt.Println("Listening on localhost:50001...")
    for {
        handleUDPConnection(conn)
    }
}
 
func handleUDPConnection(conn *net.UDPConn) {
    buffer := make([]byte, 1024)
    n, addr, err := conn.ReadFromUDP(buffer)
    if err != nil {
        fmt.Println(err)
        return
    }
    message := string(buffer[:n])
    fmt.Printf("Received message: %s from %s\n", message, addr)
 
    _, err = conn.WriteToUDP([]byte("Hello, client!"),
2024-08-12

以下是一个简单的Java TCP Socket客户端和服务器端的例子。服务器端接收客户端发送的消息并将其打印出来。

服务器端 (Server.java):




import java.io.*;
import java.net.*;
 
public class Server {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = null;
        try {
            serverSocket = new ServerSocket(1234); // 监听1234端口
            Socket socket = serverSocket.accept(); // 等待客户端连接
            System.out.println("Client connected!");
 
            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            String str = in.readLine();
            System.out.println("Client says: " + str);
 
            PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
            out.println("Server received the message.");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (serverSocket != null) {
                serverSocket.close();
            }
        }
    }
}

客户端 (Client.java):




import java.io.*;
import java.net.*;
 
public class Client {
    public static void main(String[] args) throws IOException {
        Socket socket = null;
        try {
            socket = new Socket("localhost", 1234); // 连接到本地主机的1234端口
            PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
 
            out.println("Hello from Client"); // 发送消息
            System.out.println("Client says: Hello from Client");
 
            String response = in.readLine(); // 接收响应
            System.out.println("Server says: " + response);
        } catch (UnknownHostException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (socket != null) {
                socket.close();
            }
        }
    }
}

在运行这些代码之前,请确保没有其他服务占用1234端口。运行服务器端 (Server.java) 将会创建一个监听1234端口的服务器,然后运行客户端 (Client.java) 将会连接到服务器并发送消息。服务器接收到消息后,将响应客户端并关闭连接。

2024-08-09

'# 【Linux】TCP套接字编程

一、背景与问题

在Linux系统中,网络通信是操作系统与外部世界交互的核心机制之一。TCP套接字编程作为实现可靠网络通信的基础技术,广泛应用于分布式系统、微服务架构、物联网设备等场景。理解其底层原理和实现细节,是构建高性能网络服务的关键。

传统开发中常见的问题包括:

  1. 网络连接异常(如连接超时、数据丢失)
  2. 多连接处理能力不足导致的性能瓶颈
  3. 缺乏对底层协议的深入理解导致的误用
  4. 安全性漏洞(如缓冲区溢出、未授权访问)

二、基本原理

1. TCP协议特性

TCP(Transmission Control Protocol)是面向连接的、可靠的、基于字节流的传输层协议。其核心机制包括:

  • 三次握手:建立连接时的同步过程
  • 滑动窗口机制:流量控制和拥塞控制
  • 确认应答机制:确保数据可靠传输
  • 超时重传:处理网络丢包
  • 流量控制:通过窗口大小调节数据传输速率

2. 套接字编程模型

Linux系统通过socket()系统调用创建套接字,其核心流程如下:

创建套接字 -> 绑定地址 -> 监听连接 -> 接受连接 -> 读写数据 -> 关闭连接

关键系统调用包括:

  • socket():创建套接字
  • bind():绑定本地地址
  • listen():监听连接请求
  • accept():接受连接
  • read()/write():数据传输
  • close():关闭套接字

三、环境准备

# 安装开发工具
sudo apt install build-essential

# 创建项目目录
mkdir tcp_socket_demo
cd tcp_socket_demo

四、核心实现

1. 基础通信示例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define PORT 8080
#define MAX_MSG 1024

int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int addrlen = sizeof(address);
    char buffer[MAX_MSG] = {0};

    // 创建套接字
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 设置地址重用
    int opt = 1;
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, (const void*)&opt, sizeof(opt)) < 0) {
        perror("setsockopt failed");
        exit(EXIT_FAILURE);
    }

    // 绑定地址
    address.sin_family = AF_INET;
    address.sin_port = htons(PORT);
    address.sin_addr.s_addr = INADDR_ANY;

    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

    // 监听连接
    if (listen(server_fd, 3) < 0) {
        perror("listen failed");
        exit(EXIT_FAILURE);
    }

    // 接受连接
    if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
        perror("accept failed");
        exit(EXIT_FAILURE);
    }

    // 读取数据
    int valread = read(new_socket, buffer, MAX_MSG);
    printf("Received: %s\n", buffer);

    // 发送响应
    const char *response = "Message received";
    send(new_socket, response, strlen(response), 0);
    printf("Sent: %s\n", response);

    // 关闭套接字
    close(new_socket);
    close(server_fd);
    return 0;
}

关键代码解释:

  1. setsockopt设置SO_REUSEADDR选项,允许快速重启服务
  2. bind绑定到所有网络接口(INADDR_ANY)
  3. listen设置连接队列长度
  4. accept阻塞等待客户端连接
  5. 使用read/write进行字节流通信

2. 多连接处理示例

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define PORT 8080
#define MAX_CLIENTS 10
#define MAX_MSG 1024

void* client_handler(void* arg) {
    int client_socket = *(int*)arg;
    char buffer[MAX_MSG] = {0};
    
    // 读取数据
    int valread = read(client_socket, buffer, MAX_MSG);
    printf("Client %d: %s\n", client_socket, buffer);
    
    // 发送响应
    const char *response = "Message received";
    send(client_socket, response, strlen(response), 0);
    
    // 关闭连接
    close(client_socket);
    pthread_exit(NULL);
}

int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int addrlen = sizeof(address);
    int client_sockets[MAX_CLIENTS];
    pthread_t threads[MAX_CLIENTS];
    
    // 创建套接字
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 设置地址重用
    int opt = 1;
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, (const void*)&opt, sizeof(opt)) < 0) {
        perror("setsockopt failed");
        exit(EXIT_FAILURE);
    }

    // 绑定地址
    address.sin_family = AF_INET;
    address.sin_port = htons(PORT);
    address.sin_addr.s_addr = INADDR_ANY;

    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

    // 监听连接
    if (listen(server_fd, MAX_CLIENTS) < 0) {
        perror("listen failed");
        exit(EXIT_FAILURE);
    }

    // 接受连接
    int client_index = 0;
    while (1) {
        if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
            perror("accept failed");
            exit(EXIT_FAILURE);
        }

        client_sockets[client_index++] = new_socket;

        // 创建线程处理连接
        if (pthread_create(&threads[client_index-1], NULL, client_handler, &client_sockets[client_index-1]) < 0) {
            perror("thread creation failed");
            exit(EXIT_FAILURE);
        }

        printf("Client connected\n");
    }

    // 关闭套接字
    close(server_fd);
    return 0;
}

3. 异步IO示例(使用epoll)

#include <sys/epoll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define PORT 8080
#define MAX_EVENTS 10
#define MAX_MSG 1024

int main() {
    int server_fd, epoll_fd;
    struct sockaddr_in address;
    int addrlen = sizeof(address);
    char buffer[MAX_MSG] = {0};
    
    // 创建套接字
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 设置地址重用
    int opt = 1;
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, (const void*)&opt, sizeof(opt)) < 0) {
        perror("setsockopt failed");
        exit(EXIT_FAILURE);
    }

    // 绑定地址
    address.sin_family = AF_INET;
    address.sin_port = htons(PORT);
    address.sin_addr.s_addr = INADDR_ANY;

    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

    // 监听连接
    if (listen(server_fd, 3) < 0) {
        perror("listen failed");
        exit(EXIT_FAILURE);
    }

    // 创建epoll实例
    epoll_fd = epoll_create(MAX_EVENTS);
    if (epoll_fd == -1) {
        perror("epoll_create failed");
        exit(EXIT_FAILURE);
    }

    // 添加监听事件
    struct epoll_event event;
    event.events = EPOLLIN;
    event.data.fd = server_fd;
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event) < 0) {
        perror("epoll_ctl failed");
        exit(EXIT_FAILURE);
    }

    // 处理事件
    while (1) {
        struct epoll_event events[MAX_EVENTS];
        int n = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
        
        for (int i = 0; i < n; i++) {
            if (events[i].data.fd == server_fd) {
                // 接受新连接
                int new_socket;
                if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
                    perror("accept failed");
                    continue;
                }

                // 添加新连接到epoll
                event.events = EPOLLIN | EPOLLET;
                event.data.fd = new_socket;
                if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, new_socket, &event) < 0) {
                    perror("epoll_ctl failed");
                    close(new_socket);
                }
            } else {
                // 处理数据
                int valread = read(events[i].data.fd, buffer, MAX_MSG);
                if (valread > 0) {
                    printf("Received: %s\n", buffer);
                    const char *response = "Message received";
                    send(events[i].data.fd, response, strlen(response), 0);
                } else {
                    close(events[i].data.fd);
                }
            }
        }
    }

    // 关闭套接字
    close(server_fd);
    return 0;
}

五、完整案例:多客户端聊天服务器

1. 项目结构

tcp_chat_server/
├── server/
│   ├── main.c
│   └── Makefile
├── client/
│   ├── main.c
│   └── Makefile
└── README.md

2. 服务器端代码(server/main.c)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <pthread.h>

#define MAX_CLIENTS 100
#define MAX_MSG 1024
#define PORT 8080

typedef struct {
    int fd;
    char name[64];
} client_t;

void* client_handler(void* arg) {
    client_t* client = (client_t*)arg;
    char buffer[MAX_MSG] = {0};
    int valread = read(client->fd, buffer, MAX_MSG);
    
    printf("Client %s: %s\n", client->name, buffer);
    
    const char* response = "Message received";
    send(client->fd, response, strlen(response), 0);
    
    close(client->fd);
    free(client);
    pthread_exit(NULL);
}

int main() {
    int server_fd, epoll_fd;
    struct sockaddr_in address;
    int addrlen = sizeof(address);
    char buffer[MAX_MSG] = {0};
    int client_index = 0;
    client_t* clients[MAX_CLIENTS];
    
    // 创建套接字
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 设置地址重用
    int opt = 1;
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, (const void*)&opt, sizeof(opt)) < 0) {
        perror("setsockopt failed");
        exit(EXIT_FAILURE);
    }

    // 绑定地址
    address.sin_family = AF_INET;
    address.sin_port = htons(PORT);
    address.sin_addr.s_addr = INADDR_ANY;

    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

    // 监听连接
    if (listen(server_fd, MAX_CLIENTS) < 0) {
        perror("listen failed");
        exit(EXIT_FAILURE);
    }

    // 创建epoll实例
    epoll_fd = epoll_create(MAX_CLIENTS);
    if (epoll_fd == -1) {
        perror("epoll_create failed");
        exit(EXIT_FAILURE);
    }

    // 添加监听事件
    struct epoll_event event;
    event.events = EPOLLIN;
    event.data.fd = server_fd;
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event) < 0) {
        perror("epoll_ctl failed");
        exit(EXIT_FAILURE);
    }

    // 处理事件
    while (1) {
        struct epoll_event events[MAX_CLIENTS];
        int n = epoll_wait(epoll_fd, events, MAX_CLIENTS, -1);
        
        for (int i = 0; i < n; i++) {
            if (events[i].data.fd == server_fd) {
                // 接受新连接
                int new_socket;
                if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
                    perror("accept failed");
                    continue;
                }

                // 创建客户端结构
                client_t* client = (client_t*)malloc(sizeof(client_t));
                client->fd = new_socket;
                snprintf(client->name, 64, "Client-%d", client_index++);
                clients[client_index - 1] = client;

                // 添加新连接到epoll
                event.events = EPOLLIN | EPOLLET;
                event.data.fd = new_socket;
                if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, new_socket, &event) < 0) {
                    perror("epoll_ctl failed");
                    close(new_socket);
                    free(client);
                }
            } else {
                // 处理数据
                int valread = read(events[i].data.fd, buffer, MAX_MSG);
                if (valread > 0) {
                    char* name = (char*)events[i].data.ptr;
                    printf("Client %s: %s\n", name, buffer);
                    const char* response = "Message received";
                    send(events[i].data.fd, response, strlen(response), 0);
                } else {
                    // 关闭连接
                    int fd = events[i].data.fd;
                    for (int j = 0; j < client_index; j++) {
                        if (clients[j]->fd == fd) {
                            free(clients[j]);
                            clients[j] = NULL;
                            break;
                        }
                    }
                    close(fd);
                }
            }
        }
    }

    // 关闭套接字
    close(server_fd);
    return 0;
}

3. 客户端代码(client/main.c)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define PORT 8080
#define MAX_MSG 1024

int main(int argc, char* argv[]) {
    if (argc < 2) {
        fprintf(stderr, "Usage: %s <client_id>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");

    if (connect(sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("connect failed");
        exit(EXIT_FAILURE);
    }

    char buffer[MAX_MSG] = {0};
    snprintf(buffer, MAX_MSG, "Hello from client %s", argv[1]);
    send(sock, buffer, strlen(buffer), 0);
    printf("Sent: %s\n", buffer);

    int valread = read(sock, buffer, MAX_MSG);
    printf("Received: %s\n", buffer);

    close(sock);
    return 0;
}

六、源码解析

1. epoll机制分析

epoll通过事件驱动的方式处理大量连接,其核心优势在于:

  • 零拷贝机制
  • 事件通知机制
  • 支持边缘触发和水平触发模式

关键代码:

struct epoll_event event;
event.events = EPOLLIN | EPOLLET;
event.data.fd = new_socket;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, new_socket, &event);

2. TCP滑动窗口机制

在数据传输过程中,通过窗口大小调节数据流:

  • window_size = min(rwnd, mss)
  • 每次发送数据量受限于窗口大小

七、进阶使用

1. 线程池模型

#include <pthread.h>
#define MAX_THREAD 10
pthread_t threads[MAX_THREAD];

void* thread_func(void* arg) {
    while (1) {
        struct epoll_event event;
        int n = epoll_wait(epoll_fd, &event, 1, -1);
        // 处理事件...
    }
}

2. 消息队列优化

#include <queue>
std::queue<std::string> message_queue;

void* client_handler(void* arg) {
    char buffer[MAX_MSG] = {0};
    int valread = read(client_fd, buffer, MAX_MSG);
    message_queue.push(buffer);
    // 处理消息...
}

八、性能与工程实践

1. 性能优化方案

优化策略说明效果
非阻塞IO使用fcntl()设置O_NONBLOCK支持并发处理
多线程每个连接分配独立线程提升并发能力
epoll使用事件驱动模型支持百万级连接
消息队列缓存待处理消息降低系统调用次数

2. 安全考虑

  • 使用sendmsg()替代send()处理复杂消息
  • 设置SO_REUSEPORT防止端口争用
  • 使用SOCK_CLOEXEC防止文件描述符泄露
  • 验证所有输入数据,防止缓冲区溢出

九、常见问题与踩坑

1. 常见错误及解决办法

错误场景现象解决方案
端口占用bind failed设置SO_REUSEADDR
连接超时connect failed检查防火墙规则
数据丢失数据接收不完整使用recv()循环读取
内存泄漏程序崩溃使用free()释放资源
线程死锁线程卡死使用pthread_mutex_lock保护共享资源

2. 常见性能问题

  • Nagle算法:使用TCP_NODELAY禁用,适用于小数据传输
  • 窗口大小:调整SO_RCVBUF/SO_SNDBUF优化缓冲区
  • 连接保持:使用keepalive选项维持长连接

十、最佳实践

  1. 连接管理:

    • 使用SO_REUSEADDR避免端口占用
    • 设置合理的SO_RCVBUF/SO_SNDBUF大小
    • 使用epoll或kqueue处理大量连接
  2. 数据处理:

    • 使用read()/write()进行字节流处理
    • 实现协议解析器(如JSON、Protobuf)
    • 使用sendmsg()处理复杂消息
  3. 安全实践:

    • 验证所有输入数据
    • 设置SOCK_CLOEXEC防止文件描述符泄露
    • 使用getsockopt()获取连接状态
    • 限制最大连接数

十一、总结

TCP套接字编程是Linux网络通信的基石,理解其底层原理和实现细节对于构建高性能网络服务至关重要。本文深入解析了:

  • TCP协议的核心机制
  • 套接字编程的基本流程
  • 多连接处理的实现方案
  • 异步IO模型的应用
  • 性能优化策略
  • 安全风险防控

在实际开发中,应根据具体场景选择合适的实现方式:

  • 简单场景:使用阻塞IO和单线程处理
  • 中等规模:采用线程池模型
  • 高并发场景:使用epoll或kqueue实现事件驱动
  • 分布式系统:结合gRPC或WebSocket进行优化

需要注意避免常见误区,如过度使用多线程导致的资源竞争,或忽视安全防护导致的系统漏洞。通过合理设计和优化,可以构建出稳定、高效的网络通信系统。

2024-08-08

'# LINUX下TCPING安装与使用

一、背景与问题

在Linux系统中,网络连接的健康检查是运维工作中不可或缺的环节。传统工具如telnet、nc(netcat)虽然能够完成端口连通性检测,但存在诸多局限性:

  1. 功能局限:无法精确控制超时时间、协议类型等关键参数
  2. 依赖问题:部分系统默认未安装telnet客户端
  3. 安全性缺陷:明文传输可能导致敏感信息泄露
  4. 协议兼容性:对TCP/UDP协议支持不够完善

为解决这些问题,本文将实现一个基于Python的TCPING工具,通过深度定制网络连接参数,提供更精确的网络诊断能力。该工具将支持IPv4/IPv6、TCP/UDP协议、超时控制、协议类型指定等高级功能。

二、基本原理

TCPING的核心原理是通过创建套接字连接目标主机的指定端口,并根据协议类型发送探测包。其技术要点包括:

  1. Socket编程:使用socket库创建TCP/UDP连接
  2. 超时控制:通过settimeout()设置连接和读取超时
  3. 协议区分:通过socket.SOCK_STREAM(TCP)和socket.SOCK_DGRAM(UDP)区分协议类型
  4. 异常处理:捕获socket.error、socket.timeout等异常
  5. 协议探测:发送特定协议的探测数据包(如TCP的SYN包)

三、环境准备

  1. 系统要求:Linux系统(推荐Ubuntu 20.04或更高版本)
  2. 依赖安装:

    sudo apt-get update
    sudo apt-get install python3
  3. Python环境:确保Python3已安装,建议使用虚拟环境:

    python3 -m venv tcping_env
    source tcping_env/bin/activate

四、核心实现

4.1 基础功能实现

import socket
import sys

def tcping(host, port, protocol='tcp', timeout=3):
    """
    TCPING核心实现
    
    Args:
        host (str): 目标主机IP或域名
        port (int): 目标端口号
        protocol (str): 协议类型('tcp'/'udp')
        timeout (float): 超时时间(秒)
    
    Returns:
        str: 检测结果('success'/'fail'/'timeout'/'unknown')
    """
    try:
        # 创建套接字
        sock = socket.socket(socket.AF_INET if ':' not in host else socket.AF_INET6,
                            socket.SOCK_STREAM if protocol == 'tcp' else socket.SOCK_DGRAM)
        
        # 设置超时
        sock.settimeout(timeout)
        
        # IPv6地址处理
        if ':' in host:
            host = socket.getaddrinfo(host, port, socket.AF_INET6)[0][4][0]
        else:
            host = socket.getaddrinfo(host, port, socket.AF_INET)[0][4][0]
        
        # 发送探测包(TCP发送空数据包,UDP发送1字节)
        if protocol == 'tcp':
            sock.connect((host, port))
            sock.send(b'')
        else:
            sock.sendto(b'X', (host, port))
        
        # 接收响应(仅TCP需要)
        if protocol == 'tcp':
            data = sock.recv(1024)
            if data:
                return 'success'
            else:
                return 'fail'
        return 'success'
    
    except socket.timeout:
        return 'timeout'
    except socket.error as e:
        if e.errno == socket.ENETUNREACH:
            return 'fail'
        elif e.errno == socket.ECONNREFUSED:
            return 'fail'
        else:
            return 'unknown'
    finally:
        if 'sock' in locals():
            sock.close()

if __name__ == '__main__':
    import argparse
    
    parser = argparse.ArgumentParser(description='TCPING工具')
    parser.add_argument('-H', '--host', required=True, help='目标主机')
    parser.add_argument('-p', '--port', type=int, required=True, help='目标端口')
    parser.add_argument('-t', '--timeout', type=float, default=3, help='超时时间')
    parser.add_argument('-u', '--udp', action='store_true', help='使用UDP协议')
    args = parser.parse_args()
    
    result = tcping(args.host, args.port, 'udp' if args.udp else 'tcp', args.timeout)
    print(f"检测结果: {result}")

4.2 代码逐段解释

  1. 套接字创建:根据主机地址自动选择IPv4/IPv6协议

    socket.AF_INET if ':' not in host else socket.AF_INET6
  2. 协议选择:通过socket.SOCK_STREAM(TCP)和socket.SOCK_DGRAM(UDP)区分协议类型
  3. IPv6处理:通过getaddrinfo()获取IPv6地址(需注意IPv6地址格式)
  4. 探测包发送:
  5. TCP:发送空数据包(模拟SYN包)
  6. UDP:发送单字节数据包(模拟UDP探测)
  7. 异常处理:
  8. socket.timeout:超时处理
  9. socket.error:处理网络错误(如主机不可达、端口被拒绝等)

五、完整案例

5.1 案例描述

检测某服务器的SSH端口(22)和HTTP端口(80)连通性,并记录响应时间

5.2 完整代码

import socket
import time
import sys

def tcping(host, port, protocol='tcp', timeout=3):
    """...(同上)..."""

def main():
    import argparse
    
    parser = argparse.ArgumentParser(description='TCPING工具')
    parser.add_argument('-H', '--host', required=True, help='目标主机')
    parser.add_argument('-p', '--port', type=int, required=True, help='目标端口')
    parser.add_argument('-t', '--timeout', type=float, default=3, help='超时时间')
    parser.add_argument('-u', '--udp', action='store_true', help='使用UDP协议')
    parser.add_argument('-v', '--verbose', action='store_true', help='详细输出')
    args = parser.parse_args()
    
    result = tcping(args.host, args.port, 'udp' if args.udp else 'tcp', args.timeout)
    
    if args.verbose:
        print(f"检测结果: {result}")
        print(f"主机: {args.host}")
        print(f"端口: {args.port}")
        print(f"协议: {args.udp and 'UDP' or 'TCP'}")
        print(f"超时: {args.timeout}秒")
    
    # 记录响应时间
    start_time = time.time()
    result = tcping(args.host, args.port, 'udp' if args.udp else 'tcp', args.timeout)
    elapsed = time.time() - start_time
    
    print(f"检测结果: {result}")
    print(f"响应时间: {elapsed:.2f}秒")

if __name__ == '__main__':
    main()

5.3 使用示例

# 检测TCP端口
python3 tcping.py -H 192.168.1.100 -p 22 -t 5

# 检测UDP端口
python3 tcping.py -H 192.168.1.100 -p 53 -u -t 3

# 详细输出模式
python3 tcping.py -H 192.168.1.100 -p 80 -v

六、源码解析

6.1 套接字创建机制

socket.socket(socket.AF_INET if ':' not in host else socket.AF_INET6,
             socket.SOCK_STREAM if protocol == 'tcp' else socket.SOCK_DGRAM)
  • IPv4/IPv6自动识别:通过检查主机地址是否包含:来判断IPv6
  • 协议类型选择:通过参数指定TCP或UDP协议

6.2 超时控制

sock.settimeout(timeout)
  • 设置套接字的读写超时时间
  • 适用于所有协议类型
  • 可避免长时间等待

6.3 IPv6地址处理

host = socket.getaddrinfo(host, port, socket.AF_INET6)[0][4][0]
  • 使用getaddrinfo()获取IPv6地址信息
  • 返回的地址格式为('::1', 0, 0, 0, ('::1', 0, 0, 0))等
  • 提取第一个IPv6地址作为连接目标

七、进阶使用

7.1 支持更多协议

可扩展支持SCTP、RAW套接字等协议,通过修改创建套接字的参数:

socket.socket(socket.AF_INET, socket.SOCK_SCTP)

7.2 支持SSL/TLS

添加SSL层支持,检测HTTPS端口:

import ssl

context = ssl.create_default_context()
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sslsock = context.wrap_socket(sock, server_hostname=host)

7.3 支持协议指纹识别

通过分析响应数据包,识别服务类型:

if protocol == 'tcp':
    data = sock.recv(1024)
    if data.startswith(b'220'):
        print("SSH服务")
    elif data.startswith(b'HTTP/1.1'):
        print("HTTP服务")
    else:
        print("未知服务")

八、性能与工程实践

8.1 性能优化

  1. 异步IO:使用asyncio实现异步检测
  2. 连接复用:在批量检测时复用套接字
  3. 缓存机制:缓存最近检测结果
  4. 并发控制:限制同时检测的连接数

8.2 异常处理优化

try:
    # ... 连接逻辑 ...
except socket.gaierror as e:
    print(f"DNS解析错误: {e}")
except socket.herror as e:
    print(f"主机名解析错误: {e}")
except socket.error as e:
    print(f"网络错误: {e}")

8.3 安全风险分析

  1. 协议探测风险:发送的探测包可能被防火墙识别
  2. 端口暴露风险:频繁探测可能导致被标记为攻击
  3. 数据泄露风险:发送的探测包可能包含敏感信息

建议:

  • 使用随机化探测包内容
  • 设置合理的探测频率
  • 在生产环境禁用详细输出模式

九、常见问题与踩坑

9.1 常见错误

错误类型原因解决方案
socket.gaierrorDNS解析失败检查主机名拼写,尝试使用IP地址
socket.timeout超时增加超时时间或检查网络状况
ConnectionRefused端口未开放检查服务是否运行,检查防火墙规则
AddressNotAvailable地址不可用检查网络接口配置,尝试其他主机

9.2 常见坑点

  1. IPv6支持不足:部分系统默认不支持IPv6,需手动配置
  2. 协议类型混淆:UDP探测可能被防火墙过滤
  3. 超时设置不当:过短的超时可能导致误判
  4. 多线程并发:未处理套接字资源竞争问题

9.3 错误示例与改进

错误示例:

sock.connect((host, port))  # 未处理IPv6地址

改进:

# 获取IPv6地址
host = socket.getaddrinfo(host, port, socket.AF_INET6)[0][4][0]
sock.connect((host, port))

十、最佳实践

  1. 生产环境建议:

    • 使用异步IO实现批量检测
    • 添加日志记录功能
    • 实现结果缓存机制
    • 设置合理的超时时间(建议3-5秒)
  2. 安全建议:

    • 禁用详细输出模式
    • 对敏感信息进行加密处理
    • 添加访问控制机制
    • 定期更新探测包内容
  3. 性能优化建议:

    • 使用连接池技术
    • 实现并发控制
    • 添加结果缓存
    • 优化协议探测方式

十一、总结

TCPING工具作为网络诊断的高级手段,提供了比传统工具更精确的网络状态检测能力。通过定制化实现,可以满足不同场景下的检测需求。在实际项目中,建议:

应该使用的情况:

  • 需要精确控制超时时间的场景
  • 需要支持IPv6和多种协议的场景
  • 需要安全审计的场景
  • 需要批量检测的场景

不应该使用的情况:

  • 需要实时性要求极高的场景
  • 需要处理大量并发连接的场景
  • 需要处理复杂协议的场景
  • 需要处理加密通信的场景

通过合理使用TCPING工具,可以显著提升网络运维的效率和准确性。在实际应用中,建议结合其他监控工具(如Zabbix、Prometheus)形成完整的网络监控体系。