我们将分步进行,从最基础的 TCP 通信开始,并解释关键点。

核心概念:TCP/IP 模型
无论是 Java 还是 C,Socket 编程都遵循 TCP/IP 模型的应用层协议,我们主要关注的是 TCP(面向连接、可靠传输)和 UDP(无连接、不可靠传输),这里我们以最常用的 TCP 为例。
一个完整的网络通信流程包括:
-
服务器端:
- 创建一个 Socket。
- 将 Socket 绑定到一个具体的 IP 地址和端口号。
- 开始监听来自客户端的连接请求。
- 接受客户端的连接,建立一个新的 Socket 用于与该客户端通信。
- 通过这个新的 Socket 进行数据的读写。
- 关闭连接。
-
客户端:
(图片来源网络,侵删)- 创建一个 Socket。
- 向服务器的 IP 地址和端口号发起连接请求。
- 如果连接成功,通过这个 Socket 进行数据的读写。
- 关闭连接。
第一部分:C 语言实现服务器端
C 语言使用的是 Berkeley Sockets (BSD Sockets) API,这是 Socket 编程的鼻祖。
server.c 代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h> // 用于 read, write, close
#include <sys/socket.h> // 用于 socket, bind, listen, accept
#include <netinet/in.h> // 用于 struct sockaddr_in
#include <arpa/inet.h> // 用于 inet_pton
#define PORT 8080
#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 文件描述符
// AF_INET: IPv4
// SOCK_STREAM: TCP
// 0: 自动选择协议
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 2. 设置 socket 选项,允许地址重用
// 避免 "Address already in use" 错误
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
perror("setsockopt");
exit(EXIT_FAILURE);
}
// 3. 绑定地址和端口
address.sin_family = AF_INET; // IPv4
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");
exit(EXIT_FAILURE);
}
// 4. 开始监听连接
if (listen(server_fd, 3) < 0) { // 3 是最大连接队列长度
perror("listen");
exit(EXIT_FAILURE);
}
printf("C Server listening on port %d...\n", PORT);
// 5. 接受一个新连接
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("accept");
exit(EXIT_FAILURE);
}
// 6. 读取客户端发送的数据
int valread = read(new_socket, buffer, BUFFER_SIZE);
printf("Message from Java client: %s\n", buffer);
// 7. 向客户端发送响应
char *response = "Hello from C Server!";
send(new_socket, response, strlen(response), 0);
printf("Hello message sent\n");
// 8. 关闭 socket
close(new_socket);
close(server_fd);
return 0;
}
C 服务器端编译与运行
# 使用 gcc 编译 gcc server.c -o server # 运行服务器 (它会阻塞,等待连接) ./server
第二部分:Java 实现客户端
Java 提供了更高级、更面向对象的 Socket API,在 java.net 包中。
Client.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 Client {
public static void main(String[] args) {
String hostname = "127.0.0.1"; // C 服务器的 IP 地址,本地回环
int port = 8080;
try (Socket socket = new Socket(hostname, port)) {
System.out.println("Connected to C Server.");
// 获取输出流,用于发送数据到服务器
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
// 获取输入流,用于接收服务器的数据
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
// 发送消息给 C 服务器
String message = "Hello from Java Client!";
System.out.println("Sending to C Server: " + message);
out.println(message);
// 读取 C 服务器的响应
String response = in.readLine();
System.out.println("Response from C Server: " + response);
} catch (UnknownHostException e) {
System.err.println("Don't know about host " + hostname);
System.exit(1);
} catch (IOException e) {
System.err.println("Couldn't get I/O for the connection to " +
hostname + ". Please ensure the C server is running.");
System.exit(1);
}
}
}
Java 客户端编译与运行
# 编译 Java 文件 javac Client.java # 运行 Java 客户端 java Client
运行流程与结果
-
启动 C 服务器:
./server
输出:
C Server listening on port 8080...(程序在此等待)
(图片来源网络,侵删) -
启动 Java 客户端:
java Client
-
观察输出:
- Java 客户端控制台:
Connected to C Server. Sending to C Server: Hello from Java Client! Response from C Server: Hello from C Server! - C 服务器控制台:
C Server listening on port 8080... Message from Java client: Hello from Java Client! Hello message sent
- Java 客户端控制台:
关键差异与注意事项
| 特性 | C (Berkeley Sockets) | Java |
|---|---|---|
| API 风格 | 过程式,使用 C 库函数和文件描述符(整数) | 面向对象,使用 Socket, ServerSocket, InputStream/OutputStream 等类 |
| 错误处理 | 通过函数返回值(如 -1)和 errno 全局变量来处理错误 |
通过抛出 IOException 等异常来处理错误 |
| 字节序 | 必须手动处理。htons() (host to network short), ntohs() (network to host short) 等。 |
自动处理。DataInputStream 和 DataOutputStream 会自动进行字节序转换,让开发者专注于业务逻辑。 |
| 数据类型 | 基本数据类型(int, char*) |
所有数据都是对象,流读写的是字节,需要手动序列化/反序列化。 |
| 管理资源 | 手动管理,必须调用 close() 来关闭文件描述符,否则会导致资源泄漏。 |
推荐使用 try-with-resources,实现了 AutoCloseable 接口的资源(如 Socket, InputStream)会自动关闭,非常安全。 |
| 平台依赖 | 高度依赖操作系统,Windows 使用 Winsock,与 Linux/macOS 的 BSD Sockets 有细微差别。 | 一次编写,到处运行,JVM 封装了底层的系统差异。 |
深入探讨:数据类型处理(一个常见的坑)
上面的例子只传输了字符串(字节流),如果需要传输 int, double 等基本数据类型,会遇到一个经典问题:字节序。
- 字节序:数据在内存中存储的字节顺序。
- 大端序:高位字节存储在低地址。
- 小端序:低位字节存储在低地址。
- 网络标准:网络字节序统一使用大端序。
C 语言发送一个整数
int number = 12345; // 在发送前,必须将主机字节序转换为网络字节序 int network_number = htonl(number); // host to long network send(new_socket, &network_number, sizeof(int), 0);
Java 语言接收一个整数
// DataInputStream 会自动读取 4 个字节并进行网络字节序到主机字节序的转换
DataInputStream in = new DataInputStream(socket.getInputStream());
int receivedNumber = in.readInt(); // 自动处理字节序
System.out.println("Received number: " + receivedNumber);
反之,Java 发送整数,C 接收时也需要做相应转换,Java 的 DataOutputStream.writeInt() 会自动将主机字节序转换为网络字节序。
| C 服务器 | Java 客户端 | |
|---|---|---|
| 角色 | 服务端 | 客户端 |
| 核心 API | socket(), bind(), listen(), accept(), read(), write(), close() |
Socket(), getInputStream(), getOutputStream(), close() |
| 字节序 | 手动处理 (htonl, ntohl) |
自动处理 (DataInputStream) |
| 资源管理 | 手动 close() |
推荐 try-with-resources |
| 错误处理 | 返回值检查 | 异常处理 |
通过这个例子,你应该能清晰地理解 Java 和 C 在 Socket 编程上的核心区别和实现方式,C 语言更底层,需要处理更多细节;Java 语言更高级,封装了复杂性,提供了更安全、更便捷的编程模型,两者结合使用时,关键在于处理好数据的格式和字节序问题。
