我们将分步进行,从最基础的 TCP 通信 开始,因为 TCP 是可靠的、面向连接的,更适合大多数应用场景,我们再简要介绍 UDP 通信 的区别。

核心概念回顾
在进行代码实现前,我们先快速回顾一下 Socket 编程的核心步骤,这对于理解 C 和 Java 的实现至关重要。
TCP 通信流程
服务器端
- 创建套接字:
socket() - 绑定地址和端口:
bind() - 监听连接:
listen() - 接受连接:
accept()(会阻塞,直到有客户端连接) - 收发数据:
read()/write()(或send()/recv()) - 关闭套接字:
close()
客户端
- 创建套接字:
socket() - 连接服务器:
connect() - 收发数据:
read()/write()(或send()/recv()) - 关闭套接字:
close()
第一部分:TCP 通信示例
我们将创建一个简单的 "Echo" 服务:客户端发送一条消息,服务器原样返回这条消息。

C 语言服务器端 (server.c)
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 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. 创建套接字 (AF_INET for IPv4, SOCK_STREAM for TCP)
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 设置套接字选项,允许地址重用
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
perror("setsockopt");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY; // 监听所有可用网络接口
address.sin_port = htons(PORT); // 将端口号从主机字节序转换为网络字节序
// 2. 绑定地址和端口
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 3. 监听连接
if (listen(server_fd, 3) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
// 4. 接受连接
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("accept");
exit(EXIT_FAILURE);
}
printf("Client connected: %s:%d\n", inet_ntoa(address.sin_addr), ntohs(address.sin_port));
// 5. 收发数据
int valread = read(new_socket, buffer, BUFFER_SIZE);
printf("Client message: %s\n", buffer);
send(new_socket, buffer, valread, 0); // 将收到的消息回写给客户端
printf("Message echoed back to client.\n");
// 6. 关闭套接字
close(new_socket);
close(server_fd);
return 0;
}
编译与运行 C 服务器:
gcc server.c -o server ./server
你会看到 "Server listening on port 8080..." 的输出。
Java 客户端 (Client.java)
Java 的 Socket 和 ServerSocket 类封装了底层的复杂操作,使得代码更简洁、面向对象。

// Client.java
import java.io.*;
import java.net.*;
public class Client {
public static void main(String[] args) {
String host = "localhost"; // 服务器地址
int port = 8080; // 服务器端口
String message = "Hello from Java Client!";
try (Socket socket = new Socket(host, port);
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
// 1. 连接服务器 (Socket构造函数会自动完成 connect)
System.out.println("Connected to server at " + host + ":" + port);
// 2. 发送数据
out.println(message);
System.out.println("Sent message: " + message);
// 3. 接收服务器返回的数据
String response = in.readLine();
System.out.println("Server response: " + response);
} catch (UnknownHostException e) {
System.err.println("Don't know about host " + host);
System.exit(1);
} catch (IOException e) {
System.err.println("Couldn't get I/O for the connection to " + host);
System.exit(1);
}
}
}
编译与运行 Java 客户端:
javac Client.java java Client
预期输出:
- Java 客户端控制台:
Connected to server at localhost:8080 Sent message: Hello from Java Client! Server response: Hello from Java Client! - C 服务器控制台:
Server listening on port 8080... Client connected: 127.0.0.1:54321 (端口号可能不同) Client message: Hello from Java Client! Message echoed back to client.
第二部分:关键差异与注意事项
在 C 和 Java 之间通信时,有几个非常重要的点需要注意,否则会导致数据解析失败。
字节序
- C 语言: 网络通信统一使用 大端字节序,在 C 中,你使用
htons()(host to network short),htonl()(host to network long) 等函数来转换整型和短整型。 - Java 语言: Java 虚拟机内部所有数据类型都使用 大端字节序,这意味着你不需要像在 C 中那样手动转换整数,当你通过
DataOutputStream.writeInt()写入一个整数时,它自动就是网络字节序,同样,DataInputStream.readInt()读取的也是大端序的整数。
在 C 和 Java 之间传递整数时,C 端必须使用 htons()/htonl(),而 Java 端则无需任何转换,直接使用 DataOutputStream/DataInputStream 即可。
字符编码
这是最常见的 Bug 来源,C 语言和 Java 默认的字符串编码可能不同。
- C 语言:
read()和write()函数处理的是原始的字节流,它们不知道字节代表什么字符,我们会使用send()和recv(),并结合inet_ntop()等函数来处理字符串,标准 C 库没有内置的、跨平台的统一编码处理。 - Java 语言:
InputStreamReader和OutputStreamWriter允许你指定字符编码。强烈建议显式指定编码,StandardCharsets.UTF_8。
最佳实践:
- Java 客户端发送时:
// 使用 UTF-8 编码将字符串转换为字节数组 byte[] messageBytes = message.getBytes(StandardCharsets.UTF_8); out.write(messageBytes);
- C 服务器接收时:
// read() 得到的是原始字节,你可以直接使用,或者如果知道是文本,可以按需转换 // 如果确定是UTF-8,可以使用一些库(如iconv)或手动处理(较复杂) // 对于简单的echo,直接回显字节即可 send(new_socket, buffer, valread, 0);
- C 客户端发送时:
// 假设 message_str 是你的 C 字符串 send(sock, message_str, strlen(message_str), 0);
- Java 服务器接收时:
// 使用 UTF-8 解码字节流为字符串 byte[] buffer = new byte[BUFFER_SIZE]; int bytesRead = in.read(buffer); String message = new String(buffer, 0, bytesRead, StandardCharsets.UTF_8);
数据结构
- C 语言: 你需要自己定义数据结构,并将其序列化为一串字节(手动拼接字段)。
- Java 语言: 你可以使用
DataOutputStream和DataInputStream来方便地读写基本数据类型(int,double,String等),它们会自动处理字节序和类型转换。
示例:发送一个包含 int 和 String 的数据包
-
Java 发送端:
try (DataOutputStream out = new DataOutputStream(socket.getOutputStream())) { int id = 123; String name = "Test User"; out.writeInt(id); // 写入一个int (4字节) out.writeUTF(name); // 写入一个UTF-8编码的字符串 } -
C 接收端:
int id; char name_buffer[256]; // read() 读取4个字节到id的地址处 // 注意:网络字节序已经是正确的,所以直接赋值即可 // 但如果是从主机字节序转换过来的,需要用 ntohl() read(new_socket, &id, sizeof(int)); // 读取字符串长度 (Java writeUTF 写入的是2字节的长度) uint16_t name_len; read(new_socket, &name_len, sizeof(uint16_t)); name_len = ntohs(name_len); // 转换回主机字节序 // 读取字符串内容 read(new_socket, name_buffer, name_len); name_buffer[name_len] = '\0'; // 添加字符串结束符 printf("Received ID: %d, Name: %s\n", id, name_buffer);
第三部分:UDP 通信简介
UDP 是无连接的、不可靠的,但传输开销小,适合实时性要求高的场景(如视频流、在线游戏)。
C 语言 UDP 服务器 (udp_server.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define PORT 8081
#define BUFFER_SIZE 1024
int main() {
int sockfd;
char buffer[BUFFER_SIZE];
struct sockaddr_in servaddr, cliaddr;
// 1. 创建数据报套接字
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = INADDR_ANY;
servaddr.sin_port = htons(PORT);
// 2. 绑定地址和端口
if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
printf("UDP Server listening on port %d...\n", PORT);
int len, n;
len = sizeof(cliaddr); // 必须初始化
// 3. 循环接收数据
while(1) {
n = recvfrom(sockfd, (char *)buffer, BUFFER_SIZE, 0, (struct sockaddr *)&cliaddr, &len);
buffer[n] = '\0';
printf("Client : %s\n", buffer);
sendto(sockfd, (const char *)buffer, n, 0, (const struct sockaddr *)&cliaddr, len);
printf("Message echoed back.\n");
}
return 0;
}
Java UDP 客户端 (UdpClient.java)
// UdpClient.java
import java.net.*;
public class UdpClient {
public static void main(String[] args) throws Exception {
DatagramSocket socket = new DatagramSocket();
InetAddress address = InetAddress.getByName("localhost");
int port = 8081;
String message = "Hello from Java UDP Client!";
byte[] sendBuffer = message.getBytes();
// 1. 创建数据包
DatagramPacket sendPacket = new DatagramPacket(sendBuffer, sendBuffer.length, address, port);
// 2. 发送数据包
socket.send(sendPacket);
System.out.println("Sent message: " + message);
// 3. 接收响应
byte[] receiveBuffer = new byte[1024];
DatagramPacket receivePacket = new DatagramPacket(receiveBuffer, receiveBuffer.length);
socket.receive(receivePacket);
String response = new String(receivePacket.getData(), 0, receivePacket.getLength());
System.out.println("Server response: " + response);
socket.close();
}
}
| 特性 | C 语言 | Java 语言 |
|---|---|---|
| 核心类/函数 | socket(), bind(), listen(), accept(), read(), write(), close() |
ServerSocket, Socket, SocketChannel (NIO), DataInputStream, DataOutputStream |
| 字节序处理 | 必须手动转换 (htons, htonl) |
自动处理 (JVM 使用大端序) |
| 字符编码 | 无内置支持,需手动处理或依赖第三方库 | 内置强大支持 (InputStreamWriter, StandardCharsets) |
| 面向对象 | 过程式,结构体 | 完全面向对象 |
| 易用性 | 复杂,需要管理所有细节 | 简洁,封装良好 |
| 适用场景 | 系统级编程、性能极致优化、与硬件交互 | 企业级应用、Web服务、快速开发 |
在进行 C 和 Java Socket 通信时,始终要明确数据格式(特别是字节序和字符编码),这是保证通信成功的关键,对于简单的文本通信,确保两端使用相同的编码(推荐 UTF-8)即可,对于复杂数据结构,最好定义一个清晰的协议,并严格遵循。
