C语言Socket网络编程详解:从入门到实战

发布于:2025-06-22 ⋅ 阅读:(20) ⋅ 点赞:(0)

本文将带你从零开始掌握C语言Socket编程的核心技术,包含TCP/UDP两种协议的完整实现代码及详细注释

一、Socket编程概述

1.1 什么是Socket?

Socket(套接字)是网络通信的编程接口,它允许不同主机上的进程进行数据交换。可以将其理解为网络通信的端点,就像电话通信中的电话机一样。

1.2 Socket的应用场景

  • 客户端/服务器模型(如Web服务器)

  • 即时通讯软件

  • 网络游戏

  • 分布式系统

  • 远程控制工具

二、核心概念解析

2.1 IP地址与端口

  • IP地址:设备的网络标识(如192.168.1.1

  • 端口号:进程的通信端点(0-65535,其中0-1023为系统保留)

2.2 TCP vs UDP

特性

TCP

UDP

连接方式

面向连接

无连接

可靠性

可靠(重传机制)

不可靠

传输效率

较低

较高

数据顺序

保证顺序

不保证顺序

适用场景

文件传输、Web浏览

视频流、实时游戏

三、Socket编程核心步骤

3.1 TCP通信流程

3.2 UDP通信流程

四、实战代码示例

4.1 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 BUFFER_SIZE 1024

int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int addrlen = sizeof(address);
    char buffer[BUFFER_SIZE] = {0};
    
    // 1. 创建socket文件描述符
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }
    
    // 2. 配置服务器地址
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY; // 接受任意IP的连接
    address.sin_port = htons(PORT);       // 端口转换为网络字节序
    
    // 3. 绑定socket到端口
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }
    
    // 4. 开始监听
    if (listen(server_fd, 3) < 0) {
        perror("listen failed");
        exit(EXIT_FAILURE);
    }
    
    printf("TCP服务器已启动,监听端口:%d\n", PORT);
    
    // 5. 接受客户端连接
    if ((new_socket = accept(server_fd, (struct sockaddr *)&address, 
                             (socklen_t*)&addrlen)) < 0) {
        perror("accept failed");
        exit(EXIT_FAILURE);
    }
    
    printf("客户端已连接: %s\n", inet_ntoa(address.sin_addr));
    
    // 6. 接收并回显数据
    while (1) {
        int valread = read(new_socket, buffer, BUFFER_SIZE);
        if (valread <= 0) break;
        
        printf("收到消息: %s\n", buffer);
        
        // 回显给客户端
        send(new_socket, buffer, strlen(buffer), 0);
        memset(buffer, 0, BUFFER_SIZE); // 清空缓冲区
    }
    
    // 7. 关闭连接
    close(new_socket);
    close(server_fd);
    return 0;
}

4.2 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 PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int sock = 0;
    struct sockaddr_in serv_addr;
    char buffer[BUFFER_SIZE] = {0};
    
    // 1. 创建socket
    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("socket creation failed");
        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");
        exit(EXIT_FAILURE);
    }
    
    // 3. 连接服务器
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        perror("connection failed");
        exit(EXIT_FAILURE);
    }
    
    printf("已连接到服务器 %s:%d\n", SERVER_IP, PORT);
    
    while (1) {
        printf("输入消息 (输入exit退出): ");
        fgets(buffer, BUFFER_SIZE, stdin);
        
        // 移除换行符
        buffer[strcspn(buffer, "\n")] = 0;
        
        // 检查退出命令
        if (strcmp(buffer, "exit") == 0) break;
        
        // 4. 发送数据到服务器
        send(sock, buffer, strlen(buffer), 0);
        printf("消息已发送\n");
        
        // 5. 接收服务器响应
        int valread = read(sock, buffer, BUFFER_SIZE);
        printf("服务器回复: %s\n", buffer);
        memset(buffer, 0, BUFFER_SIZE); // 清空缓冲区
    }
    
    // 6. 关闭连接
    close(sock);
    return 0;
}

4.3 UDP服务器实现

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int sockfd;
    char buffer[BUFFER_SIZE];
    struct sockaddr_in servaddr, cliaddr;
    
    // 1. 创建UDP socket
    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }
    
    // 2. 配置服务器地址
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = INADDR_ANY;
    servaddr.sin_port = htons(PORT);
    
    // 3. 绑定socket
    if (bind(sockfd, (const struct sockaddr *)&servaddr, 
             sizeof(servaddr)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }
    
    printf("UDP服务器已启动,监听端口:%d\n", PORT);
    
    int len, n;
    len = sizeof(cliaddr);
    
    while (1) {
        // 4. 接收客户端数据
        n = recvfrom(sockfd, (char *)buffer, BUFFER_SIZE, 
                    MSG_WAITALL, (struct sockaddr *)&cliaddr, &len);
        buffer[n] = '\0';
        printf("收到来自 %s 的消息: %s\n", 
               inet_ntoa(cliaddr.sin_addr), buffer);
        
        // 5. 发送响应
        sendto(sockfd, buffer, strlen(buffer), 
              MSG_CONFIRM, (const struct sockaddr *)&cliaddr, len);
        printf("已发送响应\n");
    }
    
    return 0;
}

五、关键函数深度解析

5.1 socket() - 创建通信端点

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

参数详解:

参数

说明

domain

协议族:AF_INET(IPv4), AF_INET6(IPv6), AF_UNIX(本地通信)

type

通信类型:SOCK_STREAM(TCP), SOCK_DGRAM(UDP), SOCK_RAW(原始套接字)

protocol

通常设为0,由系统自动选择适合的协议

注意事项:

  1. 创建套接字时不会分配具体地址

  2. 原始套接字(SOCK_RAW)需要管理员权限

  3. 不同协议族的套接字不能直接通信

5.2 bind() - 绑定地址与端口

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

地址结构体详解:

struct sockaddr_in {
    sa_family_t    sin_family;   // 地址族: AF_INET
    in_port_t      sin_port;     // 端口号(网络字节序)
    struct in_addr sin_addr;     // IPv4地址
    unsigned char  sin_zero[8];  // 填充字节
};

struct in_addr {
    uint32_t s_addr;  // IPv4地址(网络字节序)
};

注意事项:

  1. 客户端通常不需要调用bind()

  2. 端口号小于1024需要root权限

  3. INADDR_ANY表示接受所有网络接口的连接

  4. 地址和端口必须转换为网络字节序(htons/htonl)

5.3 listen() - 设置TCP监听状态

int listen(int sockfd, int backlog);

工作机制:

       TCP连接队列
+-----------------------+
| 已完成连接 | 未完成连接 |
+-----------------------+
       |         |
      accept()   SYN_RCVD
       |
ESTABLISHED

注意事项:

  1. 仅用于TCP套接字

  2. backlog参数的实际最大值由系统参数SOMAXCONN决定

  3. 调用listen()后套接字变为被动套接字

5.4 accept() - 接受TCP连接

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

使用示例:

struct sockaddr_in cli_addr;
socklen_t cli_len = sizeof(cli_addr);

int client_sock = accept(sockfd, (struct sockaddr*)&cli_addr, &cli_len);
printf("New connection from %s:%d\n", 
       inet_ntoa(cli_addr.sin_addr), 
       ntohs(cli_addr.sin_port));

注意事项:

  1. 阻塞函数,直到有新连接到达

  2. 返回的是一个新的套接字描述符

  3. 原始监听套接字继续保持监听状态

5.5 connect() - 建立TCP连接

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

连接建立过程:

客户端                            服务器
  |-------- SYN ------------>| 
  |<------- SYN+ACK --------| 
  |-------- ACK ------------>| 
         三次握手完成

注意事项:

  1. 客户端调用,用于连接TCP服务器

  2. UDP套接字也可调用connect(),但含义不同

  3. 常见错误:

    • ECONNREFUSED:目标端口无服务

    • ETIMEDOUT:连接超时

5.6 send()/recv() - TCP数据收发

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

常用flags标志:

标志

说明

0

默认行为(阻塞模式)

MSG_DONTWAIT

非阻塞操作

MSG_PEEK

查看数据但不从缓冲区移除

关键特性:

  1. TCP保证数据顺序和可靠性

  2. 发送缓冲区满时send()可能阻塞

  3. recv()返回0表示连接关闭

5.7 sendto()/recvfrom() - UDP数据收发

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);

UDP特点:

  1. 无连接:每个数据报独立发送

  2. 可能丢失或乱序

  3. 单次发送不宜超过MTU(通常1500字节)

  4. 适合广播和多播应用

5.8 close() - 关闭套接字

int close(int sockfd);

关闭过程:

TCP关闭序列(四次挥手):
客户端                            服务器
  |-------- FIN ------------>| 
  |<------- ACK ------------| 
  |<------- FIN ------------| 
  |-------- ACK ------------>| 

注意事项:

  1. 关闭套接字释放系统资源

  2. 使用shutdown()可进行更精细的控制

  3. 大量短连接服务中注意TIME_WAIT状态的影响

5.9 地址转换函数

// 字符串IP转二进制网络地址
int inet_pton(int af, const char *src, void *dst);

// 二进制网络地址转字符串IP
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

// 主机字节序转网络字节序
uint16_t htons(uint16_t hostshort);  // 端口转换
uint32_t htonl(uint32_t hostlong);   // IP地址转换

5.10 高级选项设置

int setsockopt(int sockfd, int level, int optname,
               const void *optval, socklen_t optlen);

常用选项:

选项

说明

SO_REUSEADDR

允许重用本地地址(解决地址占用问题)

SO_RCVBUF/SO_SNDBUF

接收/发送缓冲区大小

SO_KEEPALIVE

启用TCP保活机制

TCP_NODELAY

禁用Nagle算法(减少小数据包延迟)

六、常见问题及解决方案

6.1 地址已在使用 (Address already in use)

解决方法:

// 在bind()前设置SO_REUSEADDR选项
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));

6.2 数据接收不完整

解决方法:

  • TCP:使用循环接收直到获取完整数据

  • UDP:确保单次发送不超过MTU(通常1500字节)

6.3 非阻塞Socket

使用fcntl()设置非阻塞模式:

int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);

6.4 连接超时处理

// 设置连接超时为5秒
struct timeval timeout;
timeout.tv_sec = 5;
timeout.tv_usec = 0;
setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));

七、进阶学习建议

多线程/多进程服务器

  • 使用pthread_create()创建线程处理多个客户端

  • 使用fork()创建子进程处理连接

I/O多路复用

  • select/poll/epoll模型

  • 实现高并发服务器

网络安全

  • SSL/TLS加密通信

  • 使用OpenSSL库

协议设计

  • 自定义应用层协议

  • 数据序列化(如Protobuf)

八、总结

Socket编程是网络通信的基础,掌握要点:

  1. 理解TCP/UDP的核心区别及适用场景

  2. 掌握Socket API的使用顺序和参数配置

  3. 熟悉网络字节序转换函数(htonl(), ntohs()等)

  4. 正确处理边界情况和错误代码

  5. 逐步扩展为高性能网络程序

本文所有代码已在Ubuntu 22.04/GCC 11.3环境下测试通过,可直接编译运行:

gcc tcp_server.c -o tcp_server && ./tcp_server
gcc tcp_client.c -o tcp_client && ./tcp_client
gcc udp_server.c -o udp_server && ./udp_server

推荐学习资源:

  • 《UNIX网络编程 卷1:套接字联网API》

  • Beej's Guide to Network Programming(在线免费教程)

  • Linux man pages(man socket查看官方文档)

  • Wireshark网络协议分析工具实践