当前位置: 首页 > news >正文

Socket编程实战:从基础API到多线程服务器

一、Socket编程概述:网络通信的桥梁

Socket(套接字)是网络通信的端点,它提供了不同主机间进程通信的接口。在Linux系统中,Socket可以被视为一种特殊的文件描述符,通过标准的文件I/O操作来进行网络数据传输。

Socket编程的核心概念

        通信域:确定通信的协议族和地址格式

        套接字类型:定义通信的语义和特性

        协议:指定具体的传输协议

        地址:标识网络中的通信端点

学习Socket编程的重要性

        网络应用开发的基础

        理解网络协议的实现原理

        系统编程能力的重要体现

二、socket()函数详解:创建通信端点

socket()函数是Socket编程的起点,用于创建通信端点。

函数原型

int socket(int domain, int type, int protocol);

参数详解表

参数类型说明常用值示例值含义
domainint协议族/地址族AF_INET, AF_INET6, AF_UNIXAF_INET: IPv4网络通信
typeint套接字类型SOCK_STREAM, SOCK_DGRAMSOCK_STREAM: TCP流式套接字
protocolint具体协议0, IPPROTO_TCP, IPPROTO_UDP0: 根据前两个参数自动选择

常用参数组合

TCP套接字

int tcp_socket = socket(AF_INET, SOCK_STREAM, 0);

UDP套接字

int udp_socket = socket(AF_INET, SOCK_DGRAM, 0);

本地套接字

int local_socket = socket(AF_UNIX, SOCK_STREAM, 0);

返回值说明

        成功:返回非负的文件描述符                              失败:返回-1,并设置errno

记忆技巧:将socket()参数记为"族型协"——协议族、套接字类型、协议。

三、bind()函数与sockaddr_in结构体

bind()函数将套接字与特定的IP地址和端口号关联起来。

函数原型

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

sockaddr_in结构体详解

struct sockaddr_in {sa_family_t    sin_family;   // 地址族,如AF_INETin_port_t      sin_port;     // 端口号(网络字节序)struct in_addr sin_addr;     // IP地址(网络字节序)unsigned char  sin_zero[8];  // 填充字段,通常设为0
};struct in_addr {uint32_t s_addr;             // IPv4地址
};

地址设置示例

struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);                // 端口8080
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);   // 任意网络接口

bind()使用示例

int server_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
addr.sin_addr.s_addr = INADDR_ANY;if (bind(server_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {perror("bind failed");exit(EXIT_FAILURE);
}

关键点

        需要将sockaddr_in强制转换为sockaddr*

        端口和地址必须使用网络字节序(大端序)

        INADDR_ANY表示绑定到所有可用的网络接口

四、listen()与accept()函数详解

listen()函数

listen()将套接字转换为被动套接字,开始监听连接请求。

int listen(int sockfd, int backlog);

参数说明

   sockfd:已绑定的套接字描述符

    backlog:连接请求队列的最大长度

backlog参数详解

        表示已完成三次握手但尚未被accept()接受的连接数

        典型值:5-10(根据服务器负载能力调整)

        过大:浪费内核资源;过小:可能导致连接被拒绝

accept()函数

accept()从监听队列中接受连接,创建新的连接套接字。

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

参数说明

   sockfd:监听套接字描述符

   addr:输出参数,存储客户端地址信息

   addrlen:输入输出参数,指定地址结构大小

使用示例

int server_fd = socket(AF_INET, SOCK_STREAM, 0);
// ... bind操作 ...if (listen(server_fd, 5) < 0) {perror("listen failed");exit(EXIT_FAILURE);
}printf("Server listening on port 8080...\n");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 < 0) {perror("accept failed");exit(EXIT_FAILURE);
}printf("Client connected from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

五、监听套接字 vs 连接套接字

理解两种套接字的区别对于编写正确的服务器程序至关重要。

对比分析表

特性监听套接字连接套接字
创建方式socket()创建accept()返回
角色被动接受连接主动数据通信
生命周期整个服务器运行期间单个连接期间
数量通常1个每个客户端连接1个
操作bind(), listen(), accept()send(), recv(), close()
端口使用绑定特定端口使用相同端口但不同描述符

套接字状态转换

socket() → bind() → listen() → accept() → 新连接套接字↑          ↑         ↑          ↑
监听套接字  绑定地址  开始监听  接受连接

重要概念:监听套接字只负责接受新连接,不用于数据传输。每个接受的连接都会创建一个新的连接套接字专门处理该客户端通信。

六、connect()、getpeername()、close()等函数使用

connect()函数

客户端使用connect()主动连接服务器。

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

使用示例

int client_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);if (connect(client_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {perror("connect failed");exit(EXIT_FAILURE);
}
printf("Connected to server\n");

getpeername()函数

获取对端套接字的地址信息。

int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

使用场景

        服务器想知道客户端的详细信息

        调试和日志记录

        安全审计

close()函数

关闭套接字,释放资源。

int close(int fd);

重要提示

        关闭套接字会触发TCP连接终止序列

        服务器需要关闭监听套接字和所有连接套接字

        关闭后文件描述符不再有效

完整函数参考表

函数头文件参数说明返回值主要用途
socket()<sys/socket.h>domain, type, protocol套接字描述符创建通信端点
bind()<sys/socket.h>sockfd, addr, addrlen0成功/-1失败绑定地址到套接字
listen()<sys/socket.h>sockfd, backlog0成功/-1失败开始监听连接
accept()<sys/socket.h>sockfd, addr, addrlen新套接字描述符接受客户端连接
connect()<sys/socket.h>sockfd, addr, addrlen0成功/-1失败连接服务器
getpeername()<sys/socket.h>sockfd, addr, addrlen0成功/-1失败获取对端地址
close()<unistd.h>fd0成功/-1失败关闭套接字

七、多线程与pthread_detach()

多线程服务器可以同时处理多个客户端连接,提高服务器并发能力。

pthread_create()创建线程

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);

pthread_detach()分离线程

设置线程为分离状态,线程结束后自动释放资源。

int pthread_detach(pthread_t thread);

为什么使用pthread_detach()?

        分离线程不需要主线程调用pthread_join()等待

        线程结束后自动回收资源

        避免内存泄漏

多线程服务器示例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8080
#define BUFFER_SIZE 1024
void* handle_client(void* arg) {int client_fd = *(int*)arg;free(arg); // 释放动态分配的内存char buffer[BUFFER_SIZE];ssize_t bytes_read;pthread_detach(pthread_self()); // 分离线程,自动回收资源while ((bytes_read = recv(client_fd, buffer, sizeof(buffer) - 1, 0)) > 0) {buffer[bytes_read] = '\0';printf("Received: %s", buffer);send(client_fd, buffer, bytes_read, 0); // 回显数据}close(client_fd);printf("Client disconnected\n");return NULL;
}
int main() {int server_fd, *client_fd;struct sockaddr_in address;socklen_t addrlen = sizeof(address);pthread_t thread_id;if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {perror("socket failed");exit(EXIT_FAILURE);}address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons(PORT);if (bind(server_fd, (struct sockaddr*)&address, sizeof(address)) < 0) {perror("bind failed");exit(EXIT_FAILURE);}if (listen(server_fd, 10) < 0) {perror("listen");exit(EXIT_FAILURE);}printf("Multi-threaded server listening on port %d...\n", PORT);while (1) {client_fd = malloc(sizeof(int)); // 为每个客户端动态分配内存if (!client_fd) {perror("malloc failed");continue;}*client_fd = accept(server_fd, (struct sockaddr*)&address, &addrlen);if (*client_fd < 0) {perror("accept");free(client_fd);continue;}printf("New client connected: %s:%d\n", inet_ntoa(address.sin_addr), ntohs(address.sin_port));if (pthread_create(&thread_id, NULL, handle_client, client_fd) != 0) {perror("pthread_create");close(*client_fd);free(client_fd);}}close(server_fd);return 0;
}

八、完整代码示例:TCP服务器与客户端

TCP服务器完整代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8080
#define MAX_CLIENTS 10
#define BUFFER_SIZE 1024
void handle_connection(int client_fd, struct sockaddr_in client_addr) {char buffer[BUFFER_SIZE];ssize_t bytes_read;printf("Handling client %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));while ((bytes_read = recv(client_fd, buffer, sizeof(buffer) - 1, 0)) > 0) {buffer[bytes_read] = '\0';printf("From %s:%d: %s", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), buffer);if (send(client_fd, buffer, bytes_read, 0) < 0) {perror("send failed");break;}if (strncmp(buffer, "quit", 4) == 0) {printf("Client %s:%d requested to quit\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));break;}}close(client_fd);printf("Client %s:%d disconnected\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
}
int main() {int server_fd, client_fd;struct sockaddr_in server_addr, client_addr;socklen_t client_len = sizeof(client_addr);if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {perror("socket creation failed");exit(EXIT_FAILURE);}int opt = 1;if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {perror("setsockopt");exit(EXIT_FAILURE);}server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = INADDR_ANY;server_addr.sin_port = htons(PORT);if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {perror("bind failed");close(server_fd);exit(EXIT_FAILURE);}if (listen(server_fd, MAX_CLIENTS) < 0) {perror("listen failed");close(server_fd);exit(EXIT_FAILURE);}printf("TCP Echo Server listening on port %d...\n", PORT);printf("Server will echo back received messages. Send 'quit' to disconnect.\n");while (1) {client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len);if (client_fd < 0) {perror("accept failed");continue;}handle_connection(client_fd, client_addr);}close(server_fd);return 0;
}

TCP客户端完整代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 8080
#define BUFFER_SIZE 1024
int main() {int sockfd;struct sockaddr_in server_addr;char buffer[BUFFER_SIZE];ssize_t bytes_sent, bytes_received;if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {perror("socket creation failed");exit(EXIT_FAILURE);}server_addr.sin_family = AF_INET;server_addr.sin_port = htons(SERVER_PORT);if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {perror("invalid address");close(sockfd);exit(EXIT_FAILURE);}if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {perror("connection failed");close(sockfd);exit(EXIT_FAILURE);}printf("Connected to server %s:%d\n", SERVER_IP, SERVER_PORT);printf("Type messages to send to server (type 'quit' to exit):\n");while (1) {printf("Client: ");if (!fgets(buffer, sizeof(buffer), stdin)) {break;}bytes_sent = send(sockfd, buffer, strlen(buffer), 0);if (bytes_sent < 0) {perror("send failed");break;}if (strncmp(buffer, "quit", 4) == 0) {printf("Disconnecting...\n");break;}bytes_received = recv(sockfd, buffer, sizeof(buffer) - 1, 0);if (bytes_received < 0) {perror("recv failed");break;} else if (bytes_received == 0) {printf("Server closed connection\n");break;}buffer[bytes_received] = '\0';printf("Server: %s", buffer);}close(sockfd);printf("Connection closed\n");return 0;
}

九、常见面试题精析

1. socket()函数中AF_INET和PF_INET有什么区别?

参考答案
AF_INET表示地址族(Address Family),PF_INET表示协议族(Protocol Family)。在大多数实现中,它们被定义为相同的值,可以互换使用。但严格来说:

        AF_xxx用于地址结构(如sockaddr_in)

        PF_xxx用于协议族(如socket()的第一个参数)

2. TCP服务器为什么需要调用listen()函数?

参考答案
listen()函数将主动套接字转换为被动套接字,使其能够接受连接请求。具体作用:

        创建连接请求队列

        设置同时等待处理的最大连接数

        使套接字进入监听状态

3. accept()函数返回的套接字和监听套接字有什么区别?

参考答案

        监听套接字:用于接受新连接,生命周期与服务器相同

        连接套接字:用于与特定客户端通信,每个客户端连接一个

        监听套接字绑定到固定端口,连接套接字使用相同端口但不同文件描述符

4. 什么是字节序?网络编程中为什么要处理字节序?

参考答案
字节序是多字节数据在内存中的存储顺序:

        大端序:高位字节存储在低地址

        小端序:低位字节存储在低地址

网络编程使用网络字节序(大端序)保证不同架构主机间的兼容性。需要使用htons()、htonl()、ntohs()、ntohl()进行转换。

5. 多线程服务器中为什么建议使用pthread_detach()?

参考答案

        分离线程结束后自动释放资源,避免内存泄漏

        不需要主线程调用pthread_join()等待线程结束

        提高资源利用率,避免僵尸线程

        简化线程管理代码

6. 如何优雅地关闭套接字连接?

参考答案

  • 服务器端:先close()连接套接字,最后close()监听套接字

  • 客户端:调用close()关闭连接

  • 可以使用shutdown()控制关闭方向:

    SHUT_RD:关闭读端          SHUT_WR:关闭写端            SHUT_RDWR:完全关闭

7. 什么是SO_REUSEADDR选项?有什么作用?

参考答案
SO_REUSEADDR允许套接字绑定到处于TIME_WAIT状态的地址。作用:

        服务器重启后可以立即重用相同端口

        避免"Address already in use"错误

        提高服务器可用性

8. 如何实现一个并发服务器?

参考答案
几种常见的并发模型:

        多进程:fork()创建子进程处理连接

        多线程:pthread_create()创建线程处理连接

        I/O多路复用:select()/poll()/epoll()监控多个套接字

        异步I/O:aio_*系列函数

9. TCP的粘包问题如何解决?

参考答案

        固定长度:所有消息采用相同长度

        长度前缀:在消息前添加长度字段

        分隔符:使用特殊字符作为消息边界

        协议设计:设计自描述的应用层协议

10. 什么是非阻塞I/O?如何设置非阻塞套接字?

参考答案
非阻塞I/O使操作立即返回,不等待操作完成。

设置方法

#include <fcntl.h>
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);

总结

Socket编程是网络应用开发的核心技术,通过本文的学习,你应该掌握:

  1. 核心API理解:socket()、bind()、listen()、accept()等函数的作用和参数

  2. 套接字类型区分:监听套接字与连接套接字的不同角色

  3. 地址处理能力:sockaddr_in结构体的使用和字节序转换

  4. 并发编程技巧:多线程服务器的实现和线程管理

  5. 完整项目实践:TCP服务器和客户端的完整实现

  6. 面试准备充分:常见Socket编程面试题的解答

进阶学习建议

        学习I/O多路复用(select/poll/epoll)

        掌握非阻塞I/O和异步编程

        理解网络协议底层原理

        实践实际项目,如HTTP服务器、聊天程序等

http://www.dtcms.com/a/577591.html

相关文章:

  • Oracle Goldengate 同步过程的同步用户权限设置
  • Rust编程学习 - 如何理解Rust 语言提供了所有权、默认move 语义、借用、生命周期、内部可变性
  • 自学建立网站网络品牌推广费用
  • 卑鄙的网站开发公司网站地图页面模板
  • php网站如何编辑WordPress图片一行多张
  • 学Java第四十一天-------查找算法和排序算法
  • 从0到1学习Qt -- 信号和槽(二)
  • AI、闪购、造车……双十一的第十七年,京东、阿里、美团还有“新活”
  • IDEA不切换当前分支,实现跨分支合并的终极方案
  • 法考资源合集
  • Redis(四)——事务
  • 便宜的vps租用网站有哪些网站使用ftp
  • TestKeyDownBit函数和SetKeyDownBit函数和ClearKeyDownBit函数分析
  • RHCSA---权限管理
  • Flutter for HarmonyOS开发指南(二):混合开发架构与通信机制
  • 分布式分片执行原理解析
  • 自主建站全攻略:建站系统的选择指南与深度说明
  • 什么网站有做qq群排名的关键词优化价格
  • 网站内容怎么进行编写
  • SSM 房屋租赁系统
  • 网站排名优化服务公司福建省住房和城乡建设网站
  • 开发集成热门小游戏(vue+js)
  • Java-简单项目开发流程
  • 莱芜网站优化平台现在中型公司做网站用的是什么框架
  • 区块链-B站API程序系统方案
  • LVS三种模式及调度算法解析
  • MySql 9.5.0(2025)-Windows安装步骤
  • 查看 MySQL 数据库里的所有信息,包括表结构、数据内容
  • Rocky10 使用kubeadm 安装k8s 单节点
  • K8s 中的Serviceaccount