杰瑞科技汇

Java与C的Socket编程有何异同?

  1. Socket 编程核心概念:在深入代码之前,先理解基本原理。
  2. Java Socket 编程:提供客户端和服务端的完整代码示例。
  3. C 语言 Socket 编程:同样提供客户端和服务端的完整代码示例(在 Linux/Unix 环境下)。
  4. 对比与总结:比较 Java 和 C 在 Socket 编程上的异同。
  5. 进阶话题:多线程、NIO、阻塞与非阻塞等。

Socket 编程核心概念

可以把 Socket 想象成一个“网络插座”,你把一个插头(客户端)插进插座(服务端),两者之间就建立了一条可以双向通信的管道。

关键概念

  • IP 地址:网络中设备的唯一标识,就像你的家庭住址。
  • 端口号:设备上应用程序的唯一标识,就像你家楼上的几零几房间,一个 IP 地址可以有多个端口,每个端口对应一个服务。
  • 协议:通信的规则,我们主要关注两种:
    • TCP (Transmission Control Protocol)面向连接的、可靠的协议,通信前必须先建立连接(三次握手),数据传输有确认、重传和排序机制,确保数据无差错、不丢失、不重复且按序到达,就像打电话,必须先接通才能说话。
    • UDP (User Datagram Protocol)无连接的、不可靠的协议,发送数据前不需要建立连接,直接把数据包(Datagram)发出去,不保证对方一定能收到,也不保证顺序,就像寄明信片,寄出去就完事了,对方可能收不到,也可能乱序。
  • Socket:是操作系统提供的一种 API,它封装了底层的 TCP/IP 协议,应用程序通过调用 Socket API,就可以方便地进行网络通信。

通信流程(以 TCP 为例)

服务端

  1. 创建一个 Socket (socket())。
  2. 绑定 Socket 到一个 IP 地址和端口号 (bind()),这样客户端才能找到它。
  3. 监听来自客户端的连接请求 (listen())。
  4. 接受客户端的连接请求,并创建一个新的 Socket 与客户端通信 (accept()),这个 accept() 是阻塞的,它会一直等待直到有客户端连接。
  5. 通过新创建的 Socket 与客户端进行收发数据 (send()/recv()read()/write())。
  6. 关闭 Socket (close())。

客户端

  1. 创建一个 Socket (socket())。
  2. 连接到服务器的 IP 地址和端口号 (connect()),这个 connect() 也是阻塞的,直到连接成功或超时。
  3. 连接成功后,通过这个 Socket 与服务端进行收发数据 (send()/recv()read()/write())。
  4. 关闭 Socket (close())。

Java Socket 编程

Java 的 Socket API 封装得非常好,面向对象,使用起来非常简洁。

服务端代码 (JavaServer.java)

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class JavaServer {
    public static void main(String[] args) {
        int port = 8888; // 定义端口号
        try (ServerSocket serverSocket = new ServerSocket(port)) {
            System.out.println("服务器启动,正在监听端口 " + port + "...");
            // accept() 方法会阻塞,直到有客户端连接
            Socket clientSocket = serverSocket.accept();
            System.out.println("客户端已连接: " + clientSocket.getInetAddress().getHostAddress());
            // 获取输入流,用于读取客户端发送的数据
            BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
            // 获取输出流,用于向客户端发送数据
            PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
            String inputLine;
            // 循环读取客户端发送的数据
            while ((inputLine = in.readLine()) != null) {
                System.out.println("收到客户端消息: " + inputLine);
                // 如果客户端发送 "bye",则结束通信
                if ("bye".equalsIgnoreCase(inputLine)) {
                    break;
                }
                // 将收到的消息转换成大写并发送回客户端
                String response = inputLine.toUpperCase();
                out.println(response);
            }
            System.out.println("客户端已断开连接。");
        } catch (IOException e) {
            System.err.println("服务器异常: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

客户端代码 (JavaClient.java)

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
public class JavaClient {
    public static void main(String[] args) {
        String hostname = "localhost"; // 服务器的IP地址,本地测试用localhost
        int port = 8888;             // 必须与服务端端口一致
        try (Socket socket = new Socket(hostname, port)) {
            System.out.println("已连接到服务器 " + hostname + ":" + port);
            // 获取输入流,用于读取服务器发送的数据
            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            // 获取输出流,用于向服务器发送数据
            PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
            // 创建一个控制台输入读取器
            BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in));
            String userInput;
            // 循环读取用户在控制台的输入
            while ((userInput = stdIn.readLine()) != null) {
                // 将用户输入发送给服务器
                out.println(userInput);
                // 如果用户输入 "bye",则结束通信
                if ("bye".equalsIgnoreCase(userInput)) {
                    break;
                }
                // 读取服务器返回的响应
                String response = in.readLine();
                System.out.println("服务器响应: " + response);
            }
        } catch (UnknownHostException e) {
            System.err.println("不知道主机: " + hostname);
            e.printStackTrace();
        } catch (IOException e) {
            System.err.println("I/O: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

如何运行:

  1. 先编译并运行 JavaServer
  2. 然后在新的终端窗口编译并运行 JavaClient
  3. 在客户端的控制台输入文字,按回车,你会看到服务器将其转换成大写并返回,输入 bye 结束程序。

C 语言 Socket 编程

C 语言的 Socket API 是基于 BSD Socket 接口,更底层,需要手动处理很多细节,比如类型转换、字节序等。

服务端代码 (c_server.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 server_fd, new_socket;
    struct sockaddr_in address;
    int opt = 1;
    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. 设置 socket 选项,允许地址重用
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }
    address.sin_family = AF_INET; // IPv4
    address.sin_addr.s_addr = INADDR_ANY; // 监听所有可用的网络接口
    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) { // 3 是最大挂起连接数
        perror("listen");
        exit(EXIT_FAILURE);
    }
    printf("服务器启动,正在监听端口 %d...\n", PORT);
    // 5. 接受一个新连接
    if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
        perror("accept");
        exit(EXIT_FAILURE);
    }
    printf("客户端已连接: %s:%d\n", inet_ntoa(address.sin_addr), ntohs(address.sin_port));
    // 6. 与客户端通信
    int valread;
    while ((valread = read(new_socket, buffer, BUFFER_SIZE)) > 0) {
        printf("收到客户端消息: %s\n", buffer);
        // 如果客户端发送 "bye",则结束通信
        if (strncmp(buffer, "bye", 3) == 0) {
            break;
        }
        // 将收到的消息转换成大写并发送回客户端
        for (int i = 0; i < valread; i++) {
            if (buffer[i] >= 'a' && buffer[i] <= 'z') {
                buffer[i] = buffer[i] - 32; // 转换成大写
            }
        }
        send(new_socket, buffer, valread, 0);
        memset(buffer, 0, BUFFER_SIZE); // 清空缓冲区
    }
    // 7. 关闭 socket
    close(new_socket);
    close(server_fd);
    return 0;
}

客户端代码 (c_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 = 0;
    struct sockaddr_in serv_addr;
    char buffer[BUFFER_SIZE] = {0};
    char message[BUFFER_SIZE] = {0};
    // 1. 创建 socket 文件描述符
    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        printf("\n Socket creation error \n");
        return -1;
    }
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);
    // 将 IPv4 地址从文本转换成二进制形式
    if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
        printf("\nInvalid address/ Address not supported \n");
        return -1;
    }
    // 2. 连接到服务器
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        printf("\nConnection Failed \n");
        return -1;
    }
    printf("已连接到服务器 127.0.0.1:%d\n", PORT);
    // 3. 与服务器通信
    while (1) {
        printf("请输入消息 (输入 'bye' 退出): ");
        fgets(message, BUFFER_SIZE, stdin); // 从控制台读取一行
        // 发送消息给服务器
        send(sock, message, strlen(message), 0);
        // 如果用户输入 "bye",则结束通信
        if (strncmp(message, "bye", 3) == 0) {
            break;
        }
        // 读取服务器的响应
        int valread = read(sock, buffer, BUFFER_SIZE);
        buffer[valread] = '\0'; // 确保字符串正确终止
        printf("服务器响应: %s\n", buffer);
        memset(buffer, 0, BUFFER_SIZE); // 清空缓冲区
    }
    // 4. 关闭 socket
    close(sock);
    return 0;
}

如何运行 (Linux/Unix/macOS):

  1. 你需要一个 C 编译器,gcc
  2. 分别编译两个文件:
    gcc c_server.c -o c_server
    gcc c_client.c -o c_client
  3. 先运行服务端:
    ./c_server
  4. 在新的终端窗口运行客户端:
    ./c_client
  5. 在客户端输入文字,按回车,服务器会返回大写版本,输入 bye 结束。

对比与总结

特性 Java Socket C Socket
API 风格 面向对象Socket, ServerSocket, InputStream/OutputStream 等都是类。 面向过程/函数式,通过 socket(), bind(), connect() 等函数操作文件描述符。
易用性 ,API 设计清晰,自动处理了很多底层细节(如流包装、异常)。 ,需要手动管理内存、类型转换(字节序)、错误检查,代码更繁琐。
平台无关性 。“一次编写,到处运行”,只要安装了 JVM,代码就能在任何系统上运行。 ,代码依赖于特定的操作系统 API(如 Windows 的 Winsock 和 Linux 的 BSD Socket),需要跨平台移植。
底层控制 ,隐藏了太多细节,不适合需要极致性能或特殊网络配置的场景。 ,可以精细控制 socket 的每一个选项,如阻塞/非阻塞模式、超时等。
错误处理 使用 try-catch 异常处理机制。 通过函数返回值(通常是 -1 表示错误)和 errno 全局变量来判断错误。
字节序 Socket 类内部会自动处理字节序转换,开发者无需关心。 必须手动处理htons() (host to network short), ntohs() (network to host short) 等函数是必需的。
  • Java 更适合快速开发、企业级应用和跨平台场景,它将网络编程的复杂性封装在简洁的 API 之下,让你更专注于业务逻辑。
  • C 更适合系统编程、嵌入式开发、需要高性能或底层网络控制(如实现自己的协议)的场景,它提供了最大的灵活性和控制力,但代价是更高的复杂度和更繁琐的代码。

进阶话题

阻塞 vs. 非阻塞

  • 阻塞:默认模式,调用 accept()read() 等函数时,如果当前没有数据或连接,线程会暂停(阻塞),直到有事件发生,上面的例子都是阻塞模式的。
  • 非阻塞:通过设置 O_NONBLOCK 标志,这些函数会立即返回,如果没有数据,它们会返回一个错误码(如 EWOULDBLOCK),而不是等待,这需要程序员自己不断地轮询检查,非常消耗 CPU。

I/O 多路复用

这是处理高并发连接的更高效的方式,它允许你同时监视多个文件描述符(Socket),一旦其中任何一个“就绪”(可以读或写),内核就会通知你。

  • Java NIO (New I/O)
    • Channel:代替传统的 Stream,是双向的。
    • Buffer:所有数据的读写都通过 Buffer 进行。
    • Selector:核心组件,一个 Selector 可以注册多个 Channel,它会帮你检查哪些 Channel 已经就绪,从而实现单线程管理多个连接,大大提高了效率。
  • C 语言
    • select():最古老的方式,有文件描述符数量限制(通常是 1024)。
    • poll():改进了 select(),没有数量限制,但效率依然不高。
    • epoll() (Linux) / kqueue() (BSD/macOS):最高效的方式,没有数量限制,性能极佳,是构建高性能网络服务器的标准选择。

多线程

对于简单的阻塞式 Socket 服务器,一个 accept() 只能处理一个客户端,为了同时服务多个客户端,常见的方法是:

  1. 主线程负责 accept(),每当有新连接时,创建一个新线程来处理这个客户端的通信。
  2. 使用线程池来管理这些线程,避免频繁创建和销毁线程带来的开销。

这两种方法(多线程和 NIO)是构建高并发网络服务器的两种主要技术路线。

分享:
扫描分享到社交APP
上一篇
下一篇