- 核心概念回顾:确保理解 Socket 编程的基本原理。
- 通信模型:确定采用客户端/服务器模型。
- C 语言实现:作为服务器端。
- Java 实现:作为客户端。
- 关键点与注意事项:处理字节序、字符串编码、阻塞等问题。
- 完整代码示例:提供可直接运行的代码。
核心概念回顾
Socket(套接字)是网络通信的端点,两个程序通过 Socket 建立连接,然后在这个连接上发送和接收数据。

- IP 地址:网络中设备的唯一标识,如
0.0.1(本机)。 - 端口号:设备上应用程序的唯一标识,范围 0-65535,HTTP 服务常用 80 端口。
- 协议:我们主要使用 TCP(面向连接、可靠)和 UDP(无连接、不可靠),这里我们以更可靠的 TCP 为例。
- 流:数据以字节流的形式传输,发送方将数据写入输出流,接收方从输入流中读取。
通信模型
我们采用最经典的 客户端/服务器 模型。
-
服务器:
- 创建一个 Socket (
socket())。 - 将 Socket 绑定到一个 IP 地址和端口 (
bind())。 - 开始监听客户端连接 (
listen())。 - 阻塞等待,接受客户端的连接请求 (
accept()),一旦接受,就返回一个新的 Socket,专门用于与这个客户端通信。 - 通过新的 Socket 与客户端进行数据收发 (
read()/write())。 - 关闭连接 (
close())。
- 创建一个 Socket (
-
客户端:
- 创建一个 Socket (
socket())。 - 连接到服务器的 IP 地址和端口 (
connect())。 - 通过 Socket 与服务器进行数据收发 (
send()/recv()或read()/write())。 - 关闭连接 (
close())。
- 创建一个 Socket (
注意:Java 和 C 的函数名和调用方式不同,但底层逻辑是完全一致的。

C 语言实现(服务器端)
我们将创建一个 C 服务器,它监听 8888 端口,等待客户端连接,当客户端连接后,它会接收客户端发送的消息,打印出来,然后回复一个固定的消息。
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, &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) {
perror("listen");
exit(EXIT_FAILURE);
}
printf("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);
}
printf("Client connected: %s:%d\n", inet_ntoa(address.sin_addr), ntohs(address.sin_port));
// 6. 从客户端读取数据
int valread = read(new_socket, buffer, BUFFER_SIZE);
printf("Client message: %s\n", buffer);
// 7. 向客户端发送响应
char *response = "Hello from C Server!";
send(new_socket, response, strlen(response), 0);
printf("Response sent.\n");
// 8. 关闭 socket
close(new_socket);
close(server_fd);
return 0;
}
编译与运行:
# 编译 gcc server.c -o server # 运行 ./server
服务器会启动并等待连接。
Java 实现(客户端)
我们将创建一个 Java 客户端,它连接到 0.0.1:8888,向服务器发送一条消息,然后读取服务器的响应。
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";
int port = 8888;
try (Socket socket = new Socket(hostname, port)) {
System.out.println("Connected to the server.");
// 获取输出流,用于发送数据到服务器
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
// 获取输入流,用于从服务器读取数据
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
// 要发送的消息
String messageToServer = "Hello from Java Client!";
System.out.println("Sending to server: " + messageToServer);
out.println(messageToServer);
// 读取服务器的响应
String responseFromServer = in.readLine();
System.out.println("Server response: " + responseFromServer);
} 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);
System.exit(1);
}
}
}
编译与运行:
# 编译 javac Client.java # 运行 java Client
关键点与注意事项
a. 字节序
网络数据传输必须使用统一的字节序,即 网络字节序,它采用 大端序 (Big-Endian)。
- C 语言:
htonl()(Host to Network Long),htons()(Host to Network Short) 用于将主机字节序转换为网络字节序。ntohl(),ntohs()用于反向转换,在bind时,我们使用了htons(PORT)。 - Java 语言:Java 虚拟机内部默认使用大端序,
Socket的输入/输出流会自动处理字节序转换,你不需要像在 C 中那样手动调用转换函数。
b. 字符串编码
当传输字符串时,必须确保客户端和服务器使用相同的字符编码,否则会出现乱码。
- C 语言:C 语言本身没有内置的、跨平台的字符串编码处理,通常使用
char数组,并假设是 ASCII 或本地编码(如 Linux 下的 UTF-8),为了与 Java 兼容,最好在 C 中也使用 UTF-8。 - Java 语言:
String是基于 UTF-16 的。PrintWriter和BufferedReader默认使用平台的默认字符集,为了确保跨平台一致性,最好显式指定编码。// 修改后的 Java 客户端,显式使用 UTF-8 PrintWriter out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8"), true); BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
c. 阻塞
Socket 的 I/O 操作(如 C 的 read/accept,Java 的 readLine)默认是阻塞的,这意味着程序会一直等待,直到有数据到达或连接建立,对于简单的示例来说这是可以的,但对于高性能应用,需要使用多线程或非阻塞 I/O/NIO。
d. 缓冲区大小
- C 语言:你需要自己定义一个缓冲区(如
char buffer[BUFFER_SIZE]),如果接收的数据比缓冲区大,数据会被截断。 - Java 语言:
BufferedReader有自己的内部缓冲区,你不需要关心底层缓冲区的大小,它会自动处理。
完整代码示例(带编码处理)
为了确保通信的健壮性,我们对上面的代码进行优化,统一使用 UTF-8 编码。
C 服务器 (server_utf8.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>
#include <errno.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};
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);
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
if (listen(server_fd, 3) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
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));
// 读取客户端发送的UTF-8字符串
ssize_t valread = read(new_socket, buffer, BUFFER_SIZE - 1); // -1 to ensure null-termination
if (valread > 0) {
buffer[valread] = '\0'; // 确保字符串正确终止
printf("Client message (UTF-8): %s\n", buffer);
}
// 发送UTF-8响应
char *response = "你好,来自C服务器的问候!"; // 使用中文测试UTF-8
send(new_socket, response, strlen(response), 0);
printf("Response sent: %s\n", response);
close(new_socket);
close(server_fd);
return 0;
}
Java 客户端 (ClientUtf8.java)
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
public class ClientUtf8 {
public static void main(String[] args) {
String hostname = "127.0.0.1";
int port = 8888;
try (Socket socket = new Socket(hostname, port)) {
System.out.println("Connected to the server.");
// 显式使用UTF-8编码
PrintWriter out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8"), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
// 发送UTF-8字符串
String messageToServer = "你好,来自Java客户端的问候!";
System.out.println("Sending to server: " + messageToServer);
out.println(messageToServer);
// 读取服务器的UTF-8响应
String responseFromServer = in.readLine();
System.out.println("Server response: " + responseFromServer);
} 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);
e.printStackTrace();
System.exit(1);
}
}
}
运行步骤:
- 编译并运行 C 服务器:
gcc server_utf8.c -o server_utf8 && ./server_utf8 - 编译并运行 Java 客户端:
javac ClientUtf8.java && java ClientUtf8
你将看到双方都能正确显示中文字符,证明通信成功且编码处理正确。
