Socket编程入门:从概念到实战

Socket编程,说白了就是让两台计算机能互相"说话"。我刚开始接触网络编程时,总觉得这东西很神秘——数据怎么就从一台机器跑到另一台机器上了?后来才明白,Socket就是操作系统提供的一个"门",程序通过这个门把数据扔出去,或者从外面接进来。

今天咱们就把Socket的底裤扒干净,从概念到类型,再到编程流程和常用API,一步到位。

什么是Socket?

Socket这个词直译是"插座",我觉得挺形象的。你想啊,插座是电器和电网之间的接口,Socket就是应用程序和网络之间的接口。

从技术角度说,Socket是操作系统提供的一个抽象层。它封装了TCP/IP协议族的复杂性,让我们可以用文件描述符的方式操作网络通信。嗯,这里要注意:在Linux世界里,一切皆文件,Socket也不例外。

核心理解:Socket = IP地址 + 端口号 + 协议。这三者唯一确定了一个网络通信的端点。

我在项目中遇到过不少新手,上来就问"Socket和TCP是什么关系?"其实很简单:Socket是编程接口,TCP是传输协议。你用Socket这个工具,底层走的是TCP还是UDP,由你创建Socket时指定的类型决定。

Socket的两种类型

Socket类型主要分两种:流式Socket和数据报Socket。说白了就是一个面向连接,一个无连接。

特性 流式Socket (SOCK_STREAM) 数据报Socket (SOCK_DGRAM)
底层协议 TCP UDP
连接性 面向连接 无连接
数据可靠性 可靠,保证到达 不可靠,可能丢包
数据边界 无边界(字节流) 有边界(数据报)
典型应用 HTTP、FTP、SSH DNS、视频直播、游戏

你想想看,流式Socket就像打电话——先拨号建立连接,然后双方你一句我一句地聊,顺序不乱,数据完整。数据报Socket就像寄明信片——写好地址扔邮筒里,对方能不能收到、什么时候收到,你没法保证。

我的建议:如果你做文件传输、网页服务这类要求数据完整的应用,用流式Socket。如果是视频直播、在线游戏这类可以容忍少量丢包但要求低延迟的场景,数据报Socket更合适。

Socket编程流程

Socket编程的流程其实很固定,我把它总结为"服务端五步走,客户端三步走"。

服务端流程

  1. 创建Socket — 用socket()函数
  2. 绑定地址 — 用bind()函数,把IP和端口绑到Socket上
  3. 监听连接 — 用listen()函数,告诉内核可以接受连接了
  4. 接受连接 — 用accept()函数,阻塞等待客户端连上来
  5. 收发数据 — 用read()/write()或recv()/send()

客户端流程

  1. 创建Socket — 同样用socket()
  2. 连接服务端 — 用connect()函数,指定服务端的IP和端口
  3. 收发数据 — 和服务端一样

我曾经犯过一个低级错误:服务端忘了调用bind()就直接listen()了。结果listen()返回成功,但客户端怎么也连不上。查了半天才发现,没绑定地址时内核会随机分配端口,客户端连的当然不是我想要的端口。嗯,这种坑踩过一次就记住了。

注意:accept()函数会创建一个新的Socket用于和客户端通信,原来的监听Socket继续等待其他连接。很多人刚开始搞混这一点。

常用API函数详解

下面这几个函数是Socket编程的"基本功",我建议你背下来。

1. socket() — 创建Socket

#include <sys/socket.h>
int socket(int domain, int type, int protocol);

// 示例:创建TCP Socket
int sockfd = socket(AF_INET, SOCK_STREAM, 0);

// 示例:创建UDP Socket
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);

参数说明:

  • domain:协议族,AF_INET表示IPv4,AF_INET6表示IPv6
  • type:Socket类型,SOCK_STREAM流式,SOCK_DGRAM数据报
  • protocol:协议,传0表示自动选择

2. bind() — 绑定地址

#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

// 示例
struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(8080);
serv_addr.sin_addr.s_addr = INADDR_ANY;

bind(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

避坑指南:我曾经把端口号直接写成8080,没调用htons()做字节序转换。结果绑定的端口完全不对。记住:网络字节序是大端,主机字节序可能是小端,必须用htons()/htonl()转换。

3. listen() — 监听连接

#include <sys/socket.h>
int listen(int sockfd, int backlog);

// 示例:最大等待队列长度为5
listen(sockfd, 5);

backlog参数指定了连接请求队列的最大长度。如果客户端连接请求来得太快,超过这个数,多余的会被拒绝。

4. accept() — 接受连接

#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

// 示例
struct sockaddr_in cli_addr;
socklen_t cli_len = sizeof(cli_addr);
int connfd = accept(sockfd, (struct sockaddr*)&cli_addr, &cli_len);

accept()会阻塞,直到有客户端连接上来。返回的connfd就是和客户端通信的专用通道。

5. connect() — 连接服务端

#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

// 示例
struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(8080);
inet_pton(AF_INET, "192.168.1.100", &serv_addr.sin_addr);

connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

6. 数据收发函数

// 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);

// 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);

send()和recv()是TCP专用的,因为TCP是面向连接的,不需要每次指定对方地址。而UDP的sendto()和recvfrom()每次都要带上地址信息。

Socket编程核心流程图

下面这张图把整个流程串起来了,你一看就明白。

Socket编程核心流程图 服务端 socket() 创建Socket bind() 绑定地址 listen() 监听连接 accept() 接受连接 recv()/send() 收发数据 close() 关闭连接 客户端 socket() 创建Socket connect() 连接服务端 send()/recv() 收发数据 close() 关闭连接 三次握手

一个完整的TCP回射服务器示例

光说不练假把式。下面这个例子虽然简单,但包含了Socket编程的所有核心步骤。

/* 服务端代码:tcp_echo_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 1024

int main() {
    int listen_fd, conn_fd;
    struct sockaddr_in serv_addr, cli_addr;
    socklen_t cli_len = sizeof(cli_addr);
    char buffer[BUFFER_SIZE];
    
    // 1. 创建Socket
    listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd < 0) {
        perror("socket");
        exit(1);
    }
    
    // 2. 绑定地址
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);
    serv_addr.sin_addr.s_addr = INADDR_ANY;
    
    if (bind(listen_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
        perror("bind");
        exit(1);
    }
    
    // 3. 监听
    if (listen(listen_fd, 5) < 0) {
        perror("listen");
        exit(1);
    }
    
    printf("服务器已启动,监听端口 %d...\n", PORT);
    
    // 4. 接受连接
    conn_fd = accept(listen_fd, (struct sockaddr*)&cli_addr, &cli_len);
    if (conn_fd < 0) {
        perror("accept");
        exit(1);
    }
    
    printf("客户端已连接\n");
    
    // 5. 收发数据
    while (1) {
        memset(buffer, 0, BUFFER_SIZE);
        int n = recv(conn_fd, buffer, BUFFER_SIZE - 1, 0);
        if (n <= 0) {
            printf("客户端断开连接\n");
            break;
        }
        printf("收到: %s", buffer);
        send(conn_fd, buffer, n, 0);
    }
    
    close(conn_fd);
    close(listen_fd);
    return 0;
}
/* 客户端代码:tcp_echo_client.c */
#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 8888
#define BUFFER_SIZE 1024

int main() {
    int sock_fd;
    struct sockaddr_in serv_addr;
    char buffer[BUFFER_SIZE];
    
    // 1. 创建Socket
    sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (sock_fd < 0) {
        perror("socket");
        exit(1);
    }
    
    // 2. 连接服务端
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);
    inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr);
    
    if (connect(sock_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
        perror("connect");
        exit(1);
    }
    
    printf("已连接到服务器\n");
    
    // 3. 收发数据
    while (1) {
        printf("请输入消息: ");
        fgets(buffer, BUFFER_SIZE, stdin);
        
        send(sock_fd, buffer, strlen(buffer), 0);
        
        memset(buffer, 0, BUFFER_SIZE);
        recv(sock_fd, buffer, BUFFER_SIZE - 1, 0);
        printf("服务器回射: %s", buffer);
    }
    
    close(sock_fd);
    return 0;
}

运行方法:先在一个终端编译运行服务端,再在另一个终端编译运行客户端。客户端输入什么,服务端就回射什么。

总结

Socket编程说白了就是"创建、绑定、监听、接受、连接、收发、关闭"这七个动作的排列组合。流式Socket用TCP,数据报Socket用UDP,选哪个看你的业务场景。

我个人习惯在写Socket程序时,每一步都检查返回值。别嫌麻烦,网络编程里90%的bug都是因为没检查返回值导致的。你想想看,bind()失败了你还继续往下走,那后面的accept()能成功才怪。

好了,这一章的内容就到这儿。代码拿回去跑一跑,有问题随时翻回来看看这张流程图。


公众号:蓝海资料掘金营,微信deep3321