前言
不管是在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通信就先停留在这里吧。