第一部分:核心概念
在开始编码前,必须理解几个核心概念,它们是所有网络编程的基础。

-
Socket (套接字):可以看作是两个程序之间进行网络通信的“端点”,一个 Socket 由一个 IP 地址和一个端口号唯一标识,程序通过向 Socket 写入数据和从 Socket 读取数据来实现通信。
-
IP 地址:网络中设备的唯一地址,
168.1.100或localhost(代表本机)。 -
端口号:设备上应用程序的“编号”,一个 IP 地址上的设备可以同时运行多个网络服务,端口号用于区分这些服务,范围是 0-65535,0-1023 是系统保留端口,我们可以使用 1024 以上的任意端口,
8080。 -
通信流程:
(图片来源网络,侵删)- 服务器:被动等待连接,它会创建一个
ServerSocket,在指定的端口上“监听”客户端的连接请求,一旦有客户端连接,它会创建一个新的Socket与该客户端进行一对一的通信。 - 客户端:主动发起连接,它会创建一个
Socket,指定服务器的 IP 地址和端口号,向服务器发起连接请求,连接成功后,就可以通过这个Socket与服务器通信。
- 服务器:被动等待连接,它会创建一个
-
协议:Java 和 C 的 Socket API 默认都使用 TCP (传输控制协议),TCP 是面向连接的、可靠的协议,保证数据无差错、不丢失、不重复且按序到达,非常适合要求高可靠性的应用场景,我们这里的例子都基于 TCP。
第二部分:Java 服务器端
Java 的 Socket API 封装得非常好,代码相对简洁。
服务器端逻辑:
- 创建一个
ServerSocket并绑定到指定端口(如8080)。 - 调用
accept()方法,阻塞等待客户端连接。 - 当有客户端连接时,
accept()返回一个新的Socket对象,代表与该客户端的连接。 - 通过
Socket的getInputStream()和getOutputStream()获取输入流和输出流。 - 使用输入流读取客户端发送的数据,使用输出流向客户端发送数据。
- 通信结束后,关闭
Socket和ServerSocket。
代码示例 (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 = 8080;
// try-with-resources 语句可以自动关闭资源
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("Java 服务器已启动,监听端口 " + 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);
// 如果客户端发送 "exit",则退出循环
if ("exit".equalsIgnoreCase(inputLine)) {
break;
}
// 处理数据并返回响应
String response = "服务器已收到你的消息: " + inputLine;
out.println(response);
}
System.out.println("客户端断开连接。");
} catch (IOException e) {
System.err.println("服务器异常: " + e.getMessage());
e.printStackTrace();
}
}
}
第三部分:C 客户端
C 语言的 Socket API 是基于 Berkeley Sockets 的,更加底层,需要手动处理很多细节,并且需要包含特定的头文件和链接网络库。
客户端逻辑:
- 创建一个
socket。 - 调用
connect()函数,向服务器的 IP 地址和端口号发起连接。 - 使用
send()和recv()函数(或write()/read())来发送和接收数据。 - 通信结束后,关闭
socket。
代码示例 (CClient.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h> // 用于 close()
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h> // 用于 inet_addr()
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
int sock = 0;
struct sockaddr_in serv_addr;
char buffer[BUFFER_SIZE] = {0};
char message[BUFFER_SIZE];
// 1. 创建 socket
// AF_INET 表示 IPv4
// SOCK_STREAM 表示 TCP
// IPPROTO_TCP 表示 TCP 协议
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); // htons 将端口号从主机字节序转为网络字节序
// 将 IPv4 地址从文本转换为二进制形式
// "127.0.0.1" 是本机地址,如果要连接其他机器,请替换为对应的 IP
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("已连接到 Java 服务器,\n");
// 3. 通信循环
while (1) {
printf("请输入要发送的消息 (输入 'exit' 退出): ");
fgets(message, BUFFER_SIZE, stdin);
message[strcspn(message, "\n")] = 0; // 去掉 fgets 读取的换行符
// 发送消息到服务器
send(sock, message, strlen(message), 0);
printf("消息已发送: %s\n", message);
// 如果输入 "exit",则退出循环
if (strcmp(message, "exit") == 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);
printf("连接已关闭,\n");
return 0;
}
编译 C 代码:
在 Linux 或 macOS 上,你需要使用 -lsocket 和 -lnsl 选项来链接网络库,在现代系统上,可能只需要 -lpthread,但为了兼容性,通常加上这两个。
gcc CClient.c -o CClient
如果遇到链接错误,可以尝试:
gcc CClient.c -o CClient -lsocket -lnsl
第四部分:如何运行和测试
-
启动 Java 服务器: 打开一个终端,编译并运行 Java 代码。
javac JavaServer.java java JavaServer
你会看到输出:
Java 服务器已启动,监听端口 8080... -
启动 C 客户端: 打开另一个终端,编译并运行 C 代码。
gcc CClient.c -o CClient ./CClient
你会看到输出:
已连接到 Java 服务器。 请输入要发送的消息 (输入 'exit' 退出): -
进行交互:
- 在 C 客户端输入
hello,然后按回车。请输入要发送的消息 (输入 'exit' 退出): hello 消息已发送: hello 服务器响应: 服务器已收到你的消息: hello - 在 Java 服务器的终端,你会看到:
客户端已连接: 127.0.0.1 收到客户端消息: hello - 在 C 客户端再输入
你好世界,按回车,观察两个终端的输出。 - 在 C 客户端输入
exit,按回车,程序会退出,Java 服务器也会检测到连接断开。
- 在 C 客户端输入
第五部分:进阶话题与注意事项
-
数据格式与编码:
- 字符编码:上面的例子中,C 语言发送的是字节流,Java 读取时使用
InputStreamReader默认的平台编码(可能是 UTF-8 或 GBK),为了避免乱码,最好在两端都明确指定字符编码,new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8)。 - 二进制数据:如果传输的不是简单的文本(如图片、结构体),你需要自己定义数据格式(前4个字节表示数据长度,后面是具体数据),确保两端都能正确地“封包”和“解包”。
- 字符编码:上面的例子中,C 语言发送的是字节流,Java 读取时使用
-
并发处理:
- 上面的 Java 服务器一次只能处理一个客户端,如果需要同时处理多个客户端,你需要使用多线程,当
accept()返回一个Socket后,就启动一个新的Thread来处理这个客户端的通信,主线程则继续accept()下一个连接。 - C 语言处理并发则更复杂,通常需要使用
select,poll,epoll(Linux) 或kqueue(BSD/macOS) 等I/O多路复用技术,或者结合多线程/多进程。
- 上面的 Java 服务器一次只能处理一个客户端,如果需要同时处理多个客户端,你需要使用多线程,当
-
异常处理:
- 网络是不可靠的,随时可能发生断开、超时等异常,代码中必须妥善处理
IOException(Java) 和各种返回值为负的错误码 (C),read返回 0 表示对方正常关闭连接,返回 -1 表示出错。
- 网络是不可靠的,随时可能发生断开、超时等异常,代码中必须妥善处理
-
性能:
- 对于高性能要求的场景,Java NIO (New I/O) 提供了非阻塞 I/O 和选择器,可以更高效地管理大量连接,C 语言则依赖
epoll等高性能I/O模型。
- 对于高性能要求的场景,Java NIO (New I/O) 提供了非阻塞 I/O 和选择器,可以更高效地管理大量连接,C 语言则依赖
通过这个完整的例子,你已经掌握了 Java 和 C 语言进行 TCP Socket 通信的基本方法,这是构建分布式系统和网络应用的重要基石。
