Linux编程之socket入门教程 socket通讯原理

发布于:2024-09-18 ⋅ 阅读:(18) ⋅ 点赞:(0)

在Linux网络编程中,套接字Socket是进程间通信的基础,用来在网络上不同主机间进行数据的发送和接收。套接字作为一种抽象的接口,它屏蔽了底层网络协议的复杂性,使得开发者可以专注于数据的传输。以下将详细介绍Linux网络编程中的Socket,及其相关操作和函数。

客户端                                服务器端
┌─────────────┐                       ┌─────────────┐
│ 1. 创建Socket │ <────连接请求────── │ 1. 创建Socket │
└─────────────┘                       └─────────────┘
       │                                      │
       │                                      │
┌─────────────┐                       ┌─────────────┐
│ 2. 连接服务器 │─────连接确认──────>2. 绑定端口   │
└─────────────┘                       └─────────────┘
       │                                      │
       │                                      │
┌─────────────┐                       ┌─────────────┐
│ 3. 发送数据  │──────数据传输───────>3. 监听连接   │
└─────────────┘                       └─────────────┘
       │                                      │
       │                                      │
┌─────────────┐                       ┌─────────────┐
│ 4. 接收数据  │ <────响应数据────── │ 4. 接收数据   │
└─────────────┘                       └─────────────┘
       │                                      │
       ▼                                      ▼
    关闭连接                                关闭连接

1. Socket的类型

套接字根据使用的协议和功能,分为以下几类:

  • 流式套接字(SOCK_STREAM):使用TCP协议,面向连接,提供可靠的数据传输,保证数据的顺序性。
  • 数据报套接字(SOCK_DGRAM):使用UDP协议,无连接,不保证数据顺序和完整性,但效率较高。
  • 原始套接字(SOCK_RAW):允许对IP包进行底层操作,通常用于网络开发和调试。

2. Socket编程中的基本流程

无论是使用TCP还是UDP,Socket编程的一般步骤都是类似的。通常包括以下操作:

  1. 创建Socket
  2. 绑定地址
  3. 监听/连接
  4. 发送/接收数据
  5. 关闭Socket
常用的Socket函数以及各个函数的详细用法:
  • socket(): 创建套接字,返回套接字的文件描述符。
  • bind(): 将套接字与本地IP地址和端口号绑定。
  • listen(): 服务器端监听来自客户端的连接请求(仅用于TCP)。
  • accept(): 服务器端接受客户端的连接(仅用于TCP)。
  • connect(): 客户端与服务器建立连接(仅用于TCP)。
  • send(): 发送数据。
  • recv(): 接收数据。
  • sendto(): 发送数据到指定的地址(用于UDP)。
  • recvfrom(): 从指定的地址接收数据(用于UDP)。
  • close(): 关闭套接字。

a. 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
示例:
int sockfd = socket(AF_INET, SOCK_STREAM, 0);

b. bind() - 绑定套接字到地址

函数原型:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
  • sockfd:套接字文件描述符。
  • addr:要绑定的IP地址和端口号。
  • addrlen:地址结构长度。
返回值:
  • 成功:返回0
  • 失败:返回-1
示例:
struct sockaddr_in address;
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(8080);

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

c. listen() - 监听连接请求

函数原型:
int listen(int sockfd, int backlog);
参数:
  • sockfd:套接字文件描述符(TCP)。
  • backlog:未决连接的队列长度。
返回值:
  • 成功:返回0
  • 失败:返回-1
示例:
listen(sockfd, 5);

d. accept() - 接受连接请求

函数原型:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:
  • sockfd:监听的套接字。
  • addr:存储客户端的地址。
  • addrlen:地址结构的长度。
返回值:
  • 成功:返回新的套接字文件描述符。
  • 失败:返回-1
示例:
struct sockaddr_in client_addr;
socklen_t addrlen = sizeof(client_addr);
int new_socket = accept(sockfd, (struct sockaddr *)&client_addr, &addrlen);

e. connect() - 发起连接请求

函数原型:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
  • sockfd:套接字文件描述符。
  • addr:服务器地址。
  • addrlen:地址结构长度。
返回值:
  • 成功:返回0
  • 失败:返回-1
示例:
struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr);

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

f. send()recv() - 发送和接收数据

函数原型:
  • send()
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
  • recv()
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数:
  • sockfd:套接字文件描述符。
  • buf:数据缓冲区。
  • len:缓冲区长度。
  • flags:操作标志(如0)。
返回值:
  • 成功:返回发送/接收的字节数。
  • 失败:返回-1
示例:
char buffer[1024] = "Hello, Server!";
send(sockfd, buffer, strlen(buffer), 0);
recv(sockfd, buffer, 1024, 0);

g. sendto()recvfrom() - 发送和接收数据报(用于UDP)

函数原型:
  • sendto()
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
  • recvfrom()
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
参数:
  • sockfd:套接字文件描述符。
  • buf:数据缓冲区。
  • len:缓冲区长度。
  • flags:操作标志(如0)。
  • dest_addr:目标地址(用于sendto())。
  • addrlen:地址结构长度。
返回值:
  • 成功:返回发送/接收的字节数。
  • 失败:返回-1
示例:
char buffer[1024] = "Hello, UDP Server!";
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);

sendto(sockfd, buffer, strlen(buffer), 0, (struct sockaddr *)&server_addr, sizeof(server_addr));
recvfrom(sockfd, buffer, 1024, 0, (struct sockaddr *)&server_addr, &addrlen);

h. close() - 关闭套接字

函数原型:
int close(int sockfd);
参数:
  • sockfd:要关闭的套接字文件描述符。
返回值:
  • 成功:返回0
  • 失败:返回-1
示例:
close(sockfd);

i. shutdown() - 部分关闭套接字

函数原型:
int shutdown(int sockfd, int how);
参数:
  • sockfd:套接字文件描述符。
  • how:关闭操作:
    • SHUT_RD:关闭读操作。
    • SHUT_WR:关闭写操作。
    • SHUT_RDWR:同时关闭读写操作。
返回值:
  • 成功:返回0
  • 失败:返回-1
示例:
shutdown(sockfd, SHUT_WR);

这些函数为Linux网络编程中的Socket操作提供了基础。

3. 示例代码

下面是一个简单的基于Socket编程的网络通信实例,包含服务器端和客户端。该示例使用TCP协议,服务器接收来自客户端的消息并返回响应。

服务器端代码 (server.c)

#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>

#define PORT 8080

int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int addrlen = sizeof(address);
    char buffer[1024] = {0};
    const char *hello = "Hello from server";

    // 创建套接字
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("Socket failed");
        return 1;
    }

    // 绑定地址和端口
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("Bind failed");
        return 1;
    }

    // 开始监听
    if (listen(server_fd, 3) < 0) {
        perror("Listen failed");
        return 1;
    }

    printf("Server listening on port %d\n", PORT);

    // 接受客户端连接
    if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
        perror("Accept failed");
        return 1;
    }

    // 接收客户端消息
    int valread = read(new_socket, buffer, 1024);
    printf("Received from client: %s\n", buffer);

    // 发送响应给客户端
    send(new_socket, hello, strlen(hello), 0);
    printf("Hello message sent to client\n");

    // 关闭套接字
    close(new_socket);
    close(server_fd);

    return 0;
}

客户端代码 (client.c)

#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>

#define PORT 8080

int main() {
    int sock = 0;
    struct sockaddr_in serv_addr;
    const char *hello = "Hello from client";
    char buffer[1024] = {0};

    // 创建套接字
    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        printf("\nSocket creation error\n");
        return 1;
    }

    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);

    // 将服务器地址转换为二进制
    if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
        printf("\nInvalid address or Address not supported\n");
        return 1;
    }

    // 连接服务器
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        printf("\nConnection Failed\n");
        return 1;
    }

    // 发送消息给服务器
    send(sock, hello, strlen(hello), 0);
    printf("Hello message sent\n");

    // 接收服务器的响应
    int valread = read(sock, buffer, 1024);
    printf("Received from server: %s\n", buffer);

    // 关闭套接字
    close(sock);

    return 0;
}

编译步骤

  1. 创建源文件:

    • 将服务器代码保存为 server.c,客户端代码保存为 client.c
  2. 编译服务器端:
    在终端中运行以下命令:

    gcc server.c -o server
    
  3. 编译客户端:
    在终端中运行以下命令:

    gcc client.c -o client
    

运行步骤

  1. 启动服务器端:
    在终端中运行以下命令,启动服务器端程序:

    ./server
    
  2. 启动客户端:
    在另一个终端中运行以下命令,启动客户端程序:

    ./client
    

运行结果

  • 服务器端输出:

    Server listening on port 8080
    Received from client: Hello from client
    Hello message sent to client
    
  • 客户端输出:

    Hello message sent
    Received from server: Hello from server
    

总结:

  • 在这个例子中,服务器端在端口 8080 上监听客户端连接,接收到客户端的消息后,向客户端发送一条响应消息。
  • 客户端连接到服务器并发送一条消息,随后接收到服务器的响应。

此实例展示了Socket编程的基本流程,涉及到套接字的创建、绑定、监听、连接、发送和接收数据等步骤。

4. Socket的优缺点

优点:
  1. 灵活性高:

    • Socket提供了对网络通信底层的完全控制,适用于多种协议(TCP、UDP、原始套接字等)。
    • 可以处理从低级别的二进制传输到高级别的应用协议,实现自定义网络通信需求。
  2. 跨平台支持:

    • Socket API标准化,几乎所有操作系统都提供了对Socket编程的支持。
    • 适用于大多数编程语言(C/C++、Python、Java等),可以在多平台上进行开发。
  3. 适用于各种场景:

    • 无论是构建低延迟的实时应用(如视频会议、游戏),还是进行大规模分布式系统开发,Socket都能胜任。
    • 支持同步、异步、非阻塞等多种编程模式。
  4. 高性能:

    • 对于高性能需求场景(如高并发的服务器、流媒体等),Socket能够提供极高的效率,尤其是在与事件驱动的库(如epollselectpoll)结合时。
缺点:
  1. 编程复杂度高:

    • 使用Socket需要开发者理解网络协议、地址绑定、连接状态管理、错误处理等低层细节。
    • 开发者需要手动处理数据包的组装、拆分以及超时等情况,代码复杂度较高。
  2. 缺乏应用层协议支持:

    • Socket仅支持传输层协议(如TCP、UDP),不直接提供应用层协议(如HTTP、FTP)的支持。开发者需自行构建或使用其他库来实现这些协议。
    • 如果需要处理复杂的应用协议,可能会导致额外的工作。
  3. 难以扩展:

    • 对于大规模应用,Socket编程难以扩展和维护。需要手动处理并发问题,使用多线程或多进程模型,复杂性增加。
    • 负载均衡、故障恢复等高级特性需要额外设计。
  4. 跨语言通信复杂:

    • 虽然Socket本身是跨语言的,但对于不同语言间的通信,开发者必须在数据编码/解码上花费更多精力(如使用JSON、Protobuf等协议进行数据序列化)。

Socket与其他网络编程技术的对比
1. HTTP库(如libcurlrequests):
  • 优点:

    • 提供了高层次的抽象,简化了与Web服务器的交互(如处理GET、POST请求)。
    • 内建了许多应用层功能(如自动重定向、Cookie管理、SSL加密)。
    • 适合快速开发基于HTTP协议的客户端或服务器。
  • 缺点:

    • 只能处理HTTP协议,适用场景有限。
    • 性能通常较低,不适合实时或高并发需求。
2. RPC框架(如gRPC、Thrift):
  • 优点:

    • 高层抽象,提供了远程过程调用功能,开发者只需关注业务逻辑。
    • 支持多种语言、跨平台通信,内建序列化和高效通信协议(如Protobuf)。
    • 自带负载均衡、认证等功能,适合构建微服务架构。
  • 缺点:

    • 配置复杂,初始开发成本较高。
    • 不适合需要低延迟的实时通信场景。
3. 消息队列(如RabbitMQ、Kafka):
  • 优点:

    • 提供可靠的异步消息传递机制,适用于分布式系统中的消息传递和队列处理。
    • 支持消息持久化、负载均衡和重试机制,减少数据丢失风险。
  • 缺点:

    • 通信延迟较大,不适合实时应用。
    • 安装和维护较为复杂,特别是在集群环境中。
4. WebSocket:
  • 优点:

    • 提供全双工通信,适用于需要双向持续连接的应用(如即时聊天、股票行情推送)。
    • 较Socket更加简单易用,特别适合Web应用。
  • 缺点:

    • 依赖于浏览器或特定的库,不如原生Socket灵活。
    • 只支持在HTTP/HTTPS之上建立的连接,适用场景有限。
5. 高层框架(如Boost.Asio、Twisted、Node.js):
  • 优点:

    • 提供异步I/O和事件驱动模型,简化了Socket编程的复杂性。
    • 内置了对定时器、文件I/O等功能的支持,可以实现高效的网络编程。
  • 缺点:

    • 框架本身的学习成本较高,特别是对复杂的异步操作需要深入理解。
    • 封装较多,失去了一定的底层控制权。

总结:

技术 优点 缺点
Socket 灵活、跨平台、适合高并发、低延迟应用。 编程复杂、需要手动处理并发、应用层协议需要自行实现。
HTTP库 易用,处理HTTP协议快速高效。 仅限于HTTP协议,无法处理复杂通信场景。
RPC框架 高层抽象、跨语言、适合微服务架构。 初始复杂度高,不适合低延迟需求。
消息队列 异步通信、可靠性高、支持分布式系统。 延迟较大、适用场景有限。
WebSocket 双向通信、适合Web应用、全双工模式。 场景有限、需要在HTTP/HTTPS上运行。
高层框架 提供异步和事件驱动模型,简化编程。 封装较多,失去一定的灵活性。

Socket适合对网络编程有更高控制需求、需要自定义协议或追求高性能的场景,而其他高层网络编程方式更适合简化开发、快速集成应用层协议的场合。


网站公告

今日签到

点亮在社区的每一天
去签到