第2章:Socket编程入门——什么是Socket?Socket类型(TCP/UDP),Socket API函数详解

各位同学,今天我们来聊聊Socket编程。说实话,很多初学者一听到Socket就觉得头大,觉得这是网络编程里最难啃的骨头。但我想告诉你,Socket其实没那么神秘。

我刚开始做嵌入式开发那会儿,第一次接触Socket,翻了好几本教材,越看越糊涂。后来我干脆放下书,自己写了个最简单的收发程序,跑通了之后,回头再看那些概念,一下子就通了。所以今天,我也打算用这种「先动手、再理解」的方式来带你入门。

2.1 什么是Socket?

Socket,中文叫「套接字」。这个名字翻译得挺拗口的,我更喜欢把它理解成「网络通信的插座」。

你想想看,家里要用电,墙上得有个插座。设备插上去,电就通了。网络通信也一样——两个程序要互相发数据,中间得有个「插座」来连接。这个插座,就是Socket。

更准确地说,Socket是操作系统提供的一个编程接口。它把复杂的网络协议栈(TCP/IP)封装成了几个简单的函数调用。你不需要知道数据是怎么分包、怎么路由、怎么重传的,只需要调用send()recv(),数据就发出去了、收回来了。

一句话总结:Socket是应用层与传输层之间的桥梁。它让你用读写文件的方式,来读写网络数据。

2.2 Socket的类型:TCP vs UDP

Socket主要分两种:TCP SocketUDP Socket。这两种的区别,说白了就是「打电话」和「寄明信片」的区别。

特性 TCP Socket UDP Socket
连接方式 面向连接(先建立连接,再通信) 无连接(直接发数据)
可靠性 可靠传输,数据不丢失、不乱序 不可靠,可能丢包、乱序
传输效率 较低(有握手、确认、重传机制) 较高(没有额外开销)
应用场景 文件传输、网页浏览、邮件 视频直播、DNS查询、游戏
Socket类型 SOCK_STREAM SOCK_DGRAM

我在项目中遇到过这样一个坑:有个同事做设备间的状态同步,图省事用了UDP。结果现场测试时,状态更新经常丢包,设备显示的数据对不上。后来改成TCP,问题就解决了。所以我的建议是:如果你不确定用哪种,先选TCP。TCP虽然慢一点,但它靠谱。

我的个人习惯:控制类、状态同步类的通信,一律用TCP。日志上报、心跳检测这类可以容忍丢包的,才考虑UDP。

2.3 Socket API函数详解

好了,概念讲完了,咱们来看代码。Socket编程的核心API就这几个函数,我按使用顺序给你捋一遍。

2.3.1 socket()——创建Socket

这是第一步。就像你要打电话,得先有个电话机一样。

#include <sys/socket.h>
int socket(int domain, int type, int protocol);
  • domain:协议族。一般用AF_INET(IPv4)或AF_INET6(IPv6)。
  • type:Socket类型。SOCK_STREAM是TCP,SOCK_DGRAM是UDP。
  • protocol:协议号。通常填0,让系统自动选择。

返回值是一个文件描述符。如果返回-1,说明创建失败了。

注意:创建Socket后,它只是一个「空壳子」,还没有绑定任何地址和端口。接下来你需要用bind()来给它「上户口」。

2.3.2 bind()——绑定地址和端口

这个函数的作用,是把Socket和本地的IP地址、端口号绑定在一起。服务器端一般都要调用它。

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

我刚开始学的时候,经常忘记初始化struct sockaddr_in结构体,结果bind总是失败。后来养成习惯,每次都用memset()清零一下,再也没出过问题。

struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);  // 监听所有网卡
server_addr.sin_port = htons(8080);               // 端口号

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

避坑指南:我曾经遇到过bind返回EADDRINUSE错误,原因是上次程序退出后端口没有释放。解决方案是设置SO_REUSEADDR选项,允许重用地址。

2.3.3 listen()——监听连接

这是TCP服务器特有的函数。它告诉内核:这个Socket要开始监听客户端的连接请求了。

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

backlog参数表示连接请求队列的最大长度。一般设为5或10就够了。设得太大反而浪费内存。

2.3.4 accept()——接受连接

这个函数会阻塞,直到有客户端连接进来。它返回一个新的Socket文件描述符,用于和这个客户端通信。

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

注意:原来的Socket还在监听,新返回的Socket才是用来收发数据的。这个设计很巧妙——一个Socket负责「接客」,多个Socket负责「服务」。

2.3.5 connect()——发起连接

这是客户端用的函数。它向服务器发起连接请求。

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

如果服务器没开、或者网络不通,connect会返回-1。我记得有一次调试,服务器IP写错了,connect一直超时,查了半天才发现是IP少写了一位。嗯,这种低级错误,谁还没犯过呢?

2.3.6 send() 和 recv()——收发数据

连接建立之后,就可以用这两个函数来通信了。

#include <sys/socket.h>
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);
  • send():发送数据。返回实际发送的字节数。如果返回-1,说明发送失败。
  • recv():接收数据。返回实际接收的字节数。如果返回0,说明对方关闭了连接。

重要提醒:send()不一定一次就把所有数据发完,recv()也不一定一次就把所有数据收完。你需要用循环来确保数据完整传输。这个坑,我踩过不止一次。

2.4 一个完整的TCP通信示例

光说不练假把式。下面是一个最简单的TCP服务器和客户端代码,你可以在自己的开发板上跑一下试试。

服务器端(server.c):

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main() {
    int server_fd, client_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t addr_len = sizeof(client_addr);
    char buffer[1024] = {0};

    // 1. 创建Socket
    server_fd = socket(AF_INET, SOCK_STREAM, 0);

    // 2. 绑定地址
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(8080);
    bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));

    // 3. 监听
    listen(server_fd, 5);

    // 4. 接受连接
    client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &addr_len);

    // 5. 接收数据
    recv(client_fd, buffer, sizeof(buffer), 0);
    printf("收到: %s\n", buffer);

    // 6. 关闭
    close(client_fd);
    close(server_fd);
    return 0;
}

客户端(client.c):

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

int main() {
    int sockfd;
    struct sockaddr_in server_addr;
    char *message = "Hello, Socket!";

    // 1. 创建Socket
    sockfd = socket(AF_INET, SOCK_STREAM, 0);

    // 2. 设置服务器地址
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8080);
    inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);

    // 3. 连接服务器
    connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));

    // 4. 发送数据
    send(sockfd, message, strlen(message), 0);

    // 5. 关闭
    close(sockfd);
    return 0;
}

先启动服务器,再启动客户端。如果一切正常,服务器会打印出「收到: Hello, Socket!」。

我的建议:第一次跑通这个例子后,试着改一改——比如让客户端发多次数据,或者让服务器回复一条消息。动手改代码,比看十遍书都管用。

2.5 本章小结

这一章我们讲了Socket的基本概念、TCP和UDP的区别,以及六个核心API函数。说白了,Socket编程就是一套固定的流程:创建、绑定、监听、接受/连接、收发、关闭。你把这个流程记熟了,后面再学多线程、非阻塞、epoll这些高级话题,就轻松多了。

下一章,我们会用Python来写一个更实用的Socket程序,把今天学的这些API用Python的socket模块重新实现一遍。你会发现,Python写起来比C简洁太多了。

好了,今天就到这里。有问题随时问我,咱们下节课见。