Qt多进程(十一)Linux下socket通信

前言

不管是在Windows还是Linux或IOS,涉及到Qt的网络通信都首选用QTcpClient这些跨平台性良好的封装类。但早期学习Linux的时候,在非Qt环境下曾实现过socket通信,这种方式更加原始,也未见得就必须被Qt类所取代。如果是纯c环境下,socket通信的写法我觉得有必要复习一下。

一、代码示例

概念性的东西无需多说,直接上服务端和客户端的代码吧!

服务端:

cpp 复制代码
// tcp_server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define PORT 8888
#define BUFFER_SIZE 256

int main() {
    int server_fd, client_fd;
    struct sockaddr_in address;
    int addrlen = sizeof(address);
    char buffer[BUFFER_SIZE] = {0};
    const char *reply = "Message received!";

    // 1. 创建 TCP socket
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 2. 设置地址重用(避免 "Address already in use")
    int opt = 1;
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
        perror("setsockopt failed");
        exit(EXIT_FAILURE);
    }

    // 3. 绑定地址:127.0.0.1:8888
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY; // 监听所有本地接口(包括 127.0.0.1)
    address.sin_port = htons(PORT);

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

    // 4. 开始监听(最多排队 3 个连接)
    if (listen(server_fd, 3) < 0) {
        perror("listen failed");
        exit(EXIT_FAILURE);
    }

    printf("Server listening on 127.0.0.1:%d\n", PORT);

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

    // 6. 读取客户端发来的数据
    ssize_t bytes = read(client_fd, buffer, BUFFER_SIZE - 1);
    if (bytes > 0) {
        buffer[bytes] = '\0';
        printf("Server received: %s\n", buffer);
    }

    // 7. 回复客户端
    write(client_fd, reply, strlen(reply));

    // 8. 关闭连接
    close(client_fd);
    close(server_fd);
    return 0;
}
cpp 复制代码
// tcp_client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#define SERVER_IP "127.0.0.1"
#define PORT 8888
#define BUFFER_SIZE 256

int main() {
    int sock;
    struct sockaddr_in serv_addr;
    char buffer[BUFFER_SIZE] = {0};
    const char *msg = "Hello from TCP client!";

    // 1. 创建 TCP socket
    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("socket creation error");
        exit(EXIT_FAILURE);
    }

    // 2. 设置服务器地址
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);

    // 将 IP 字符串转为二进制格式
    if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) {
        perror("Invalid address / Address not supported");
        exit(EXIT_FAILURE);
    }

    // 3. 连接服务器
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        perror("Connection failed");
        exit(EXIT_FAILURE);
    }

    // 4. 发送消息
    write(sock, msg, strlen(msg));
    printf("Client sent: %s\n", msg);

    // 5. 接收服务器回复
    ssize_t bytes = read(sock, buffer, BUFFER_SIZE - 1);
    if (bytes > 0) {
        buffer[bytes] = '\0';
        printf("Client received: %s\n", buffer);
    }

    // 6. 关闭 socket
    close(sock);
    return 0;
}

运行效果:

二、代码解释

以tcp为例,服务端的话无非就是那几个步骤,创建套接字、设置地址和端口并绑定、开启监听、连接成功后开始读写通信。

客户端的话也差不多,创建套接字、连接地址和端口,连接成功后开始读写。

在上小节提供的代码中,有一些细节想要记录一下的。

1.struct sockaddr_in address;

这个结构体内储存了一些参数,包括协议簇,监听端口类型,还有端口号。在bind绑定的时候,需要传递这个结构体。这一点和qt还是不太一样的。

2.int sockfd = socket(domain, type, protocol);

怎么决定是tcp还是udp,或者是针对于QLocalSocket对应的uds(Unix Domain Socket)呢?就取决于这里传递的参数。

第一个参数:

(1)AF_INET, 它是IPv4网络通信,对应TCP/UDP都可以

(2)AF_INET6,它是IPv6 网络通信

(3)AF_UNIX或AF_LOCAL,它是本地进程通信(UDS),对应的是Unix Domain Socket

第二个参数:

(1)SOCK_STREAM,是指TCP(或 UDS 流式)这种字节流的传输,面向连接、可靠、有序、无消息边界

(2)SOCK_DGRAM,是指UDP(或 UDS 数据报)这种数据包方式,无连接、不可靠、有消息边界

(3)SOCK_SEQPACKET,是指有序数据报(较少用)

所以:

socket(AF_INET, SOCK_STREAM, 0) → TCP

socket(AF_INET, SOCK_DGRAM, 0) → UDP

socket(AF_UNIX, SOCK_STREAM, 0) → UDS(流式,类似 TCP)

socket(AF_UNIX, SOCK_DGRAM, 0) → UDS(数据报,类似 UDP)

4.阻塞点

代码中有一些会产生阻塞的地方,比如服务端调用了accept之后,会一直等待,直到有客户端调用connect来连接。、

还有一个就是read读取的时候会等待。

如果是客户单,connect会阻塞等待,直到成功或超时。read也会等待。write如果缓冲区满了也会阻塞。

三、解决阻塞问题

如果你足够敏锐的话,应该能察觉到服务端在accept之后陷入了阻塞,之后它只能等待客户端连接,不能干其他的事情。

如果是在Qt中,直接使用封装好的类和信号槽机制,轻松解决这种同步阻塞问题。

在纯c中的linux环境中,可以采用epoll机制。(select / poll / epoll)

还没深入学习,抛砖引玉一下吧。

cpp 复制代码
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/epoll.h>

#define PORT 8888
#define MAX_EVENTS 10
#define BUFFER_SIZE 256

// 设置 fd 为非阻塞
void set_nonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

int main() {
    int server_fd, epfd, nfds, i;
    struct sockaddr_in address;
    struct epoll_event ev, events[MAX_EVENTS];

    // 1. 创建 TCP socket
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    // 2. 设为非阻塞!
    set_nonblocking(server_fd);

    // 3. 设置 SO_REUSEADDR
    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    // 4. bind + listen
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);
    bind(server_fd, (struct sockaddr *)&address, sizeof(address));
    listen(server_fd, 10);

    printf("Non-blocking server started on port %d\n", PORT);
    printf("Server is NOT blocked! You can do other work here...\n");

    // 5. 创建 epoll 实例
    epfd = epoll_create1(0);
    ev.events = EPOLLIN;  // 监听"可读"事件(对 listen socket 就是"可 accept")
    ev.data.fd = server_fd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, server_fd, &ev);

    // 6. 主事件循环(模拟 Qt 的 QEventLoop)
    while (1) {
        // 这里可以插入"其他工作",比如定时任务、处理其他 fd...
        // printf("Doing other work...\n");
        // sleep(1);

        // 等待 I/O 事件(最多 1 秒超时,避免死等)
        nfds = epoll_wait(epfd, events, MAX_EVENTS, 1000); // 1000ms 超时

        for (i = 0; i < nfds; i++) {
            if (events[i].data.fd == server_fd) {
                // 有新连接!
                struct sockaddr_in client_addr;
                socklen_t client_len = sizeof(client_addr);
                int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len);

                if (client_fd != -1) {
                    printf("New client connected! (fd=%d)\n", client_fd);

                    // 回复客户端(简化:直接发完就关)
                    const char *msg = "Hello from non-blocking server!\n";
                    write(client_fd, msg, strlen(msg));
                    close(client_fd);
                }
            }
        }
    }

    close(server_fd);
    close(epfd);
    return 0;
}

四、总结

linux下的socket通信因为是传统阻塞式编程,想要实现非阻塞还需要学习更多进阶知识。而qt是现代事件驱动框架,对网络通信的支持已经相对完善和全面。由于讨论主题是多进程的进程间通信,socket通信就先停留在这里吧。

相关推荐
代码游侠2 小时前
学习笔记——ESP8266 WiFi模块
服务器·c语言·开发语言·数据结构·算法
weixin_462446232 小时前
Python 使用 PyQt5 + Pandas 实现 Excel(xlsx)批量合并工具(带图形界面)
python·qt·pandas
行者962 小时前
Flutter跨平台开发适配OpenHarmony:进度条组件的深度实践
开发语言·前端·flutter·harmonyos·鸿蒙
__雨夜星辰__2 小时前
VMware 17 下 Ubuntu 虚拟机与宿主机间复制粘贴失效问题
linux·运维·ubuntu
prettyxian2 小时前
【linux】进程调度:优先级、时间片与O(1)算法
linux·运维·服务器
DYS_房东的猫2 小时前
《 C++ 零基础入门教程》第3章:结构体与类 —— 用面向对象组织代码
开发语言·c++
向量引擎2 小时前
复刻“疯狂的鸽子”?用Python调用Sora2与Gemini-3-Pro实现全自动热点视频流水线(附源码解析)
开发语言·人工智能·python·gpt·ai·ai编程·api调用
小猪佩奇TONY2 小时前
Linux 内核学习(15) --- linux MMU 和 分页机制
linux·学习
jerryinwuhan2 小时前
期末总复习
linux·运维