Socket(套接字) 是网络通信的端点,是操作系统提供给应用程序的网络编程接口。
read()/write() 进行网络通信| 类型 | 说明 | 用途 |
|---|---|---|
SOCK_STREAM |
面向连接的可靠字节流 | TCP通信 |
SOCK_DGRAM |
无连接的数据报 | UDP通信 |
SOCK_RAW |
原始套接字 | 自定义协议、网络诊断 |
struct sockaddrstruct sockaddr {
sa_family_t sa_family; // 地址族(AF_INET/AF_INET6)
char sa_data[14]; // 协议特定地址数据
};
struct sockaddr_instruct sockaddr_in {
sa_family_t sin_family; // 地址族,必须是AF_INET
in_port_t sin_port; // 端口号(网络字节序)
struct in_addr sin_addr; // IPv4地址
char sin_zero[8]; // 填充字节,使大小与sockaddr相同
};
struct in_addr {
in_addr_t s_addr; // 32位IPv4地址(网络字节序)
};
struct sockaddr_in6struct sockaddr_in6 {
sa_family_t sin6_family; // AF_INET6
in_port_t sin6_port; // 端口号
uint32_t sin6_flowinfo; // 流信息
struct in6_addr sin6_addr; // IPv6地址
uint32_t sin6_scope_id; // 范围ID
};
struct in6_addr {
uint8_t s6_addr[16]; // 128位IPv6地址
};
// 常见错误:直接传递sockaddr_in
int ret = bind(sockfd, (struct sockaddr_in*)&addr, sizeof(addr)); // 错误!
// 正确做法:强制类型转换为sockaddr
int ret = bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)); // 正确!
字节序问题:不同CPU架构使用不同的字节存储顺序
| 函数 | 说明 | 使用场景 |
|---|---|---|
htons() |
主机→网络,16位 | 端口号 |
htonl() |
主机→网络,32位 | IPv4地址 |
ntohs() |
网络→主机,16位 | 解析端口号 |
ntohl() |
网络→主机,32位 | 解析IPv4地址 |
#include <arpa/inet.h>
uint16_t port = 8080;
uint16_t net_port = htons(port); // 主机→网络
uint32_t ip = 0xC0A80101; // 192.168.1.1
uint32_t net_ip = htonl(ip); // 主机→网络
// 字符串 → 网络字节序二进制
in_addr_t inet_addr(const char *cp);
// 网络字节序 → 字符串
char *inet_ntoa(struct in_addr in);
// 字符串 → 网络字节序二进制(推荐使用)
int inet_pton(int af, const char *src, void *dst);
// 网络字节序 → 字符串(推荐使用)
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
#include <arpa/inet.h>
// 传统方式
struct in_addr addr;
addr.s_addr = inet_addr("192.168.1.1");
char *str = inet_ntoa(addr);
// 现代方式(推荐)
struct in_addr addr;
inet_pton(AF_INET, "192.168.1.1", &addr);
char buf[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &addr, buf, sizeof(buf));
socket() → bind() → listen() → accept() → read()/write() → close()
socket() → connect() → write()/read() → close()
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
// domain: AF_INET(IPv4), AF_INET6(IPv6)
// type: SOCK_STREAM(TCP), SOCK_DGRAM(UDP)
// protocol: 0(自动选择)
// 返回值: 成功返回文件描述符,失败返回-1
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// sockfd: socket返回的文件描述符
// addr: 要绑定的地址结构指针
// addrlen: 地址结构大小
// 返回值: 成功返回0,失败返回-1
int listen(int sockfd, int backlog);
// sockfd: 已绑定的socket
// backlog: 已完成连接队列的最大长度(通常128)
// 返回值: 成功返回0,失败返回-1
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// sockfd: 监听socket
// addr: 输出参数,客户端地址信息
// addrlen: 输入输出参数,地址结构大小
// 返回值: 新的socket文件描述符(用于与客户端通信)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// sockfd: 客户端socket
// addr: 服务器地址
// addrlen: 地址结构大小
// 返回值: 成功返回0,失败返回-1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define PORT 8080
#define BUF_SIZE 1024
int main() {
int server_fd, client_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t addr_len = sizeof(client_addr);
char buf[BUF_SIZE];
// 1. 创建socket
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0) {
perror("socket");
exit(EXIT_FAILURE);
}
// 设置地址复用
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 2. 绑定地址
memset(&server_addr, 0, sizeof(server_addr));
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");
close(server_fd);
exit(EXIT_FAILURE);
}
// 3. 监听
if (listen(server_fd, 128) < 0) {
perror("listen");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
// 4. 接受连接
client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &addr_len);
if (client_fd < 0) {
perror("accept");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("Client connected: %s:%d\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port));
// 5. 通信
ssize_t n;
while ((n = read(client_fd, buf, BUF_SIZE)) > 0) {
buf[n] = '\0';
printf("Received: %s", buf);
write(client_fd, buf, n); // Echo回客户端
}
// 6. 关闭
close(client_fd);
close(server_fd);
return 0;
}
编译命令:
gcc -Wall -o server server.c
运行:
./server
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define SERVER_IP "127.0.0.1"
#define PORT 8080
#define BUF_SIZE 1024
int main() {
int sockfd;
struct sockaddr_in server_addr;
char buf[BUF_SIZE];
// 1. 创建socket
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket");
exit(EXIT_FAILURE);
}
// 2. 连接服务器
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);
if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("connect");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("Connected to server\n");
// 3. 通信
printf("Enter message: ");
fgets(buf, BUF_SIZE, stdin);
write(sockfd, buf, strlen(buf));
ssize_t n = read(sockfd, buf, BUF_SIZE);
if (n > 0) {
buf[n] = '\0';
printf("Echo: %s", buf);
}
// 4. 关闭
close(sockfd);
return 0;
}
编译命令:
gcc -Wall -o client client.c
sockaddr_in 转换为 sockaddraccept() 返回新fd,监听fd继续监听SO_REUSEADDR 避免地址占用错误inet_pton()/inet_ntop() 替代旧函数select()/poll()/epoll 实现并发服务器// 避免文件描述符泄漏
int fd = socket(...);
if (fd < 0) {
perror("socket");
return -1;
}
// 使用goto统一错误处理(推荐)
if (bind(...) < 0) goto error;
if (listen(...) < 0) goto error;
// ...
return 0;
error:
close(fd);
return -1;
socket()/bind()/listen()/accept()/connect()的作用accept()返回新fd的原因(支持并发)INADDR_ANY的含义(绑定所有本地接口)select()/poll()/epoll()的区别SO_REUSEADDR与SO_REUSEPORT的区别