????????⚡️⚡️专栏:C高手编程-面试宝典/技术手册/高手进阶⚡️⚡️????????
「C高手编程」专栏融合了作者十多年的C语言开发经验,汇集了从基础到进阶的关键知识点,是不可多得的知识宝典。如果你是即将毕业的学生,面临C语言的求职面试,本专栏将帮助你扎实地掌握核心概念,轻松应对笔试与面试;如果你已有两三年的工作经验,专栏中的内容将补充你在实践中可能忽略的新技术和技巧;而对于资深的C语言程序员,这里也将是一本实用的技术备查手册,提供全面的知识回顾与更新。无论处在哪个阶段,「C高手编程」都能助你一臂之力,成为C语言领域的行家里手。
概述
本章将详细介绍C语言中的socket编程,包括socket的创建、配置、连接和数据传输等关键概念。通过本章的学习,读者将能够掌握socket编程的基础知识,并能够在实际项目中应用这些知识构建可靠的网络应用程序。
1. Socket编程基础
1.1 Socket定义
- 定义:Socket(套接字)是用于网络通信的一种抽象接口,它允许一个进程与另一个进程进行通信,无论这两个进程是否在同一台机器上。
1.2 Socket地址结构
-
地址结构:不同的地址族(如AF_INET和AF_INET6)使用不同的地址结构。
-
struct sockaddr_in
:IPv4地址结构。 -
struct sockaddr_in6
:IPv6地址结构。
-
1.3 Socket类型
- 类型:SOCK_STREAM(流式套接字)、SOCK_DGRAM(数据报套接字)。
2. Socket创建与配置
2.1 创建Socket
2.1.1 socket
函数
-
函数原型:
int socket(int domain, int type, int protocol);
-
头文件:
<sys/socket.h>
-
参数:
- domain:地址家族(AF_INET, AF_INET6等)。
- type:套接字类型(SOCK_STREAM, SOCK_DGRAM等)。
- protocol:协议(通常为0,表示使用默认协议)。
- 返回值:成功返回一个整数(文件描述符),失败返回-1。
- 描述:此函数用于创建一个新的套接字。
2.2 配置Socket
2.2.1 bind
函数
-
函数原型:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
-
头文件:
<sys/socket.h>
-
参数:
- sockfd:套接字文件描述符。
-
addr:指向
struct sockaddr
的指针。 - addrlen:地址结构的长度。
- 返回值:成功返回0,失败返回-1。
- 描述:此函数用于将套接字与本地地址绑定。
2.3 监听连接
2.3.1 listen
函数
-
函数原型:
int listen(int sockfd, int backlog);
-
头文件:
<sys/socket.h>
-
参数:
- sockfd:套接字文件描述符。
- backlog:监听队列的最大长度。
- 返回值:成功返回0,失败返回-1。
- 描述:此函数用于将套接字设置为监听模式。
3. 客户端连接
3.1 连接服务器
3.1.1 connect
函数
-
函数原型:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
-
头文件:
<sys/socket.h>
-
参数:
- sockfd:套接字文件描述符。
-
addr:指向
struct sockaddr
的指针。 - addrlen:地址结构的长度。
- 返回值:成功返回0,失败返回-1。
- 描述:此函数用于建立与服务器的连接。
4. 接收客户端连接
4.1 接受连接
4.1.1 accept
函数
-
函数原型:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
-
头文件:
<sys/socket.h>
-
参数:
- sockfd:监听套接字文件描述符。
-
addr:指向
struct sockaddr
的指针。 - addrlen:地址结构的长度。
- 返回值:成功返回新的套接字文件描述符,失败返回-1。
- 描述:此函数用于接受客户端的连接请求。
5. 数据传输
5.1 发送数据
5.1.1 send
函数
-
函数原型:
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
-
头文件:
<sys/socket.h>
-
参数:
- sockfd:套接字文件描述符。
- buf:指向要发送的数据的指针。
- len:要发送的数据长度。
- flags:标志位。
- 返回值:成功返回发送的字节数,失败返回-1。
- 描述:此函数用于发送数据。
5.2 接收数据
5.2.1 recv
函数
-
函数原型:
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
-
头文件:
<sys/socket.h>
-
参数:
- sockfd:套接字文件描述符。
- buf:指向接收数据的指针。
- len:接收数据的最大长度。
- flags:标志位。
- 返回值:成功返回接收的字节数,失败返回-1。
- 描述:此函数用于接收数据。
6. 完整示例:服务器端
下面是一个完整的服务器端示例,该示例展示了如何使用上述函数来创建一个简单的TCP服务器,它监听一个端口并处理来自客户端的连接请求。
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#define PORT 8080
#define BUFFER_SIZE 100
int main() {
// 创建socket
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("Socket creation failed");
return 1;
}
// 配置socket
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = INADDR_ANY;
if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("Bind failed");
return 1;
}
// 开始监听
if (listen(sockfd, 5) == -1) {
perror("Listen failed");
return 1;
}
printf("Server listening on port %d\n", PORT);
while (1) {
// 接受连接
socklen_t client_len = sizeof(struct sockaddr_in);
int client_fd = accept(sockfd, (struct sockaddr *)&server_addr, &client_len);
if (client_fd == -1) {
perror("Accept failed");
continue;
}
// 接收数据
char buffer[BUFFER_SIZE];
ssize_t bytes_received = recv(client_fd, buffer, BUFFER_SIZE, 0);
if (bytes_received == -1) {
perror("Receive failed");
continue;
}
buffer[bytes_received] = '\0'; // Null-terminate the string
printf("Received from %s:%d: %s\n",
inet_ntoa(server_addr.sin_addr), ntohs(server_addr.sin_port), buffer);
// 发送响应
const char *response = "Hello, client!";
ssize_t bytes_sent = send(client_fd, response, strlen(response), 0);
if (bytes_sent == -1) {
perror("Send failed");
continue;
}
printf("Sent %zd bytes\n", bytes_sent);
// 关闭连接
close(client_fd);
}
// 关闭socket
close(sockfd);
return 0;
}
7. 完整示例:客户端端
接下来是一个客户端示例,它展示了如何使用上述函数来创建一个简单的TCP客户端,该客户端连接到服务器并发送消息。
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#define SERVER_PORT 8080
#define BUFFER_SIZE 100
int main() {
// 创建socket
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("Socket creation failed");
return 1;
}
// 配置server address
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
// 连接服务器
if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("Connect failed");
return 1;
}
// 发送数据
const char *message = "Hello, server!";
ssize_t bytes_sent = send(sockfd, message, strlen(message), 0);
if (bytes_sent == -1) {
perror("Send failed");
return 1;
}
printf("Sent %zd bytes\n", bytes_sent);
// 接收数据
char buffer[BUFFER_SIZE];
ssize_t bytes_received = recv(sockfd, buffer, BUFFER_SIZE, 0);
if (bytes_received == -1) {
perror("Receive failed");
return 1;
}
buffer[bytes_received] = '\0'; // Null-terminate the string
printf("Received %zd bytes: %s\n", bytes_received, buffer);
// 关闭socket
close(sockfd);
return 0;
}
8. 套接字选项
8.1 设置套接字选项
8.1.1 setsockopt
函数
-
函数原型:
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
-
头文件:
<sys/socket.h>
-
参数:
- sockfd:套接字文件描述符。
- level:选项级别。
- optname:选项名称。
- optval:指向选项值的指针。
- optlen:选项值的长度。
- 返回值:成功返回0,失败返回-1。
- 描述:此函数用于设置套接字选项。
8.2 获取套接字选项
8.2.1 getsockopt
函数
-
函数原型:
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
-
头文件:
<sys/socket.h>
-
参数:
- sockfd:套接字文件描述符。
- level:选项级别。
- optname:选项名称。
- optval:指向选项值的指针。
- optlen:选项值的长度。
- 返回值:成功返回0,失败返回-1。
- 描述:此函数用于获取套接字选项。
8.3 常见套接字选项
- SO_REUSEADDR:允许重新绑定到仍在TIME_WAIT状态的地址。
- SO_KEEPALIVE:保持连接活动状态。
- SO_LINGER:控制关闭套接字时的行为。
- SO_RCVBUF 和 SO_SNDBUF:设置接收和发送缓冲区大小。
8.3.1 示例
#include <sys/socket.h>
#include <stdio.h>
int main() {
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("Socket creation failed");
return 1;
}
int optval = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) == -1) {
perror("Setsockopt failed");
return 1;
}
return 0;
}
9. 多路复用
9.1 使用select
函数
9.1.1 select
函数
-
函数原型:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
-
头文件:
<sys/select.h>
-
参数:
- nfds:文件描述符的最大值加1。
- readfds:读事件的文件描述符集合。
- writefds:写事件的文件描述符集合。
- exceptfds:异常事件的文件描述符集合。
- timeout:超时时间。
- 返回值:成功返回就绪的文件描述符数量,超时返回0,失败返回-1。
- 描述:此函数用于监控多个文件描述符的就绪状态,包括读、写和异常事件。
9.2 使用poll
函数
9.2.1 poll
函数
-
函数原型:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
-
头文件:
<sys/poll.h>
-
参数:
- fds:文件描述符集合。
- nfds:文件描述符的数量。
- timeout:超时时间。
- 返回值:成功返回就绪的文件描述符数量,超时返回0,失败返回-1。
-
描述:此函数用于监控多个文件描述符的就绪状态,与
select
类似但更为高效。
9.3 使用epoll
函数
9.3.1 epoll_create
函数
-
函数原型:
int epoll_create(int size);
-
头文件:
<sys/epoll.h>
-
参数:
- size:初始事件表的大小。
- 返回值:成功返回一个整数(文件描述符),失败返回-1。
- 描述:此函数用于创建一个epoll实例。
9.3.2 epoll_ctl
函数
-
函数原型:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
-
头文件:
<sys/epoll.h>
-
参数:
- epfd:epoll文件描述符。
- op:操作类型(EPOLL_CTL_ADD, EPOLL_CTL_MOD, EPOLL_CTL_DEL)。
- fd:文件描述符。
-
event:指向
struct epoll_event
的指针。
- 返回值:成功返回0,失败返回-1。
- 描述:此函数用于添加、修改或删除监控的文件描述符。
9.3.3 epoll_wait
函数
-
函数原型:
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
-
头文件:
<sys/epoll.h>
-
参数:
- epfd:epoll文件描述符。
- events:事件数组。
- maxevents:数组的最大大小。
- timeout:超时时间。
- 返回值:成功返回就绪的文件描述符数量,超时返回0,失败返回-1。
- 描述:此函数用于等待文件描述符就绪。
9.3.4 示例
#include <sys/socket.h>
#include <sys/epoll.h>
#include <stdio.h>
#include <unistd.h>
#define MAX_EVENTS 10
#define MAX_FDS 10
int main() {
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("Socket creation failed");
return 1;
}
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = INADDR_ANY;
if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("Bind failed");
return 1;
}
if (listen(sockfd, 5) == -1) {
perror("Listen failed");
return 1;
}
int epoll_fd = epoll_create(MAX_FDS);
if (epoll_fd == -1) {
perror("Epoll create failed");
return 1;
}
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = sockfd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sockfd, &ev) == -1) {
perror("Epoll add failed");
return 1;
}
while (1) {
struct epoll_event events[MAX_EVENTS];
int num_events = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
if (num_events == -1) {
perror("Epoll wait failed");
break;
}
for (int i = 0; i < num_events; i++) {
if (events[i].data.fd == sockfd) {
// 接受连接
socklen_t client_len = sizeof(struct sockaddr_in);
int client_fd = accept(sockfd, (struct sockaddr *)&server_addr, &client_len);
if (client_fd == -1) {
perror("Accept failed");
continue;
}
// 添加客户端到epoll
ev.data.fd = client_fd;
ev.events = EPOLLIN | EPOLLET;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev) == -1) {
perror("Epoll add failed");
close(client_fd);
continue;
}
} else {
// 接收数据
char buffer[100];
ssize_t bytes_received = recv(events[i].data.fd, buffer, sizeof(buffer), 0);
if (bytes_received == -1) {
perror("Receive failed");
close(events[i].data.fd);
continue;
}
buffer[bytes_received] = '\0'; // Null-terminate the string
printf("Received %zd bytes: %s\n", bytes_received, buffer);
}
}
}
close(sockfd);
close(epoll_fd);
return 0;
}
10. 结论
本章详细介绍了C语言中的socket编程,涵盖了socket的创建、配置、连接、数据传输、套接字选项设置以及多路复用等多个方面。通过本章的学习,读者可以掌握socket编程的核心概念和技术,并能够在实际开发中构建可靠的网络应用程序。
-
Socket创建与配置:
- 使用
socket
函数创建套接字。 - 使用
bind
函数绑定套接字到特定地址。 - 使用
listen
函数使套接字进入监听状态。
- 使用
-
客户端连接与接收:
- 使用
connect
函数建立客户端与服务器之间的连接。 - 使用
accept
函数接收客户端连接请求。
- 使用
-
数据传输:
- 使用
send
函数从服务器发送数据给客户端。 - 使用
recv
函数从客户端接收数据。
- 使用
-
套接字选项:
- 使用
setsockopt
函数设置套接字选项以优化网络通信。 - 使用
getsockopt
函数获取套接字选项的状态。
- 使用
-
多路复用:
- 使用
select
函数实现非阻塞式的I/O多路复用,提高程序效率。 -
select
函数监控多个文件描述符的就绪状态,包括读、写和异常事件。
- 使用