TCP通信编程
# TCP 通信编程详解
# 1. 创建简单的客户端-服务端连接
注意:在网络编程中,我们需要养成一个良好的习惯:首先编写服务端,并先启动服务端,确保服务端可以正常监听端口,然后再编写和启动客户端。
# 1.1 服务端代码详解
package org.javatop.socket;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @description: TCP 服务端示例
* @version: 1.0
* @author: scholar
* @date: 2023-11-02
*/
public class SocketTCPServer01 {
public static void main(String[] args) throws IOException {
/*
1. 创建一个 ServerSocket 对象,绑定到指定端口(如 8888),用于监听客户端连接请求。
细节:
- 该端口必须未被其他服务占用。
- ServerSocket 可以通过 accept() 方法接受多个客户端的连接请求,支持并发。
*/
ServerSocket serverSocket = new ServerSocket(8888);
System.out.println("服务端:正在 8888 端口监听,等待客户端连接...");
// 2. accept() 方法会阻塞当前线程,直到有客户端连接到指定端口。
// 当客户端连接成功时,返回一个 Socket 对象,代表客户端的连接。
Socket socket = serverSocket.accept();
System.out.println("服务端 socket = " + socket.getClass());
// 3. 获取输入流,读取客户端发送的数据。
InputStream is = socket.getInputStream();
// 4. 使用字节数组接收数据,通过循环读取直到结束。
byte[] buf = new byte[1024];
int readLen = 0;
while ((readLen = is.read(buf)) != -1) {
// 将接收到的字节数据转换为字符串,并输出到控制台
System.out.println(new String(buf, 0, readLen));
}
// 5. 关闭流和 Socket 连接,释放资源
is.close();
socket.close();
serverSocket.close();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
代码解读:
ServerSocket(int port)
: 创建一个绑定到指定端口的服务器套接字,端口号必须未被其他服务占用。serverSocket.accept()
: 该方法会阻塞当前线程,直到有客户端连接成功后返回一个与客户端通信的Socket
对象。socket.getInputStream()
: 获取与客户端连接的输入流,用于读取客户端发送的数据。
# 1.2 客户端代码详解
package org.javatop.socket;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
/**
* @description: TCP 客户端示例
* @version: 1.0
* @author: scholar
* @date: 2023-11-02
*/
public class SocketTCPClient01 {
public static void main(String[] args) throws IOException {
/*
1. 创建一个 Socket 对象,连接到服务端的 IP 地址和端口号。
连接成功后,返回一个 Socket 对象。
- 参数1: InetAddress 表示服务端的 IP 地址,可以通过 getLocalHost() 获取本机地址。
- 参数2: int 表示服务端的端口号(如 8888)。
*/
Socket socket = new Socket(InetAddress.getLocalHost(), 8888);
System.out.println("客户端 socket = " + socket.getClass());
// 2. 获取输出流,向服务端发送数据
OutputStream outputStream = socket.getOutputStream();
// 3. 通过输出流发送数据
outputStream.write("Hello, SocketTCPServer01! 这是客户端发送的一条消息。".getBytes());
// 4. 关闭流和 Socket 连接,释放资源
outputStream.close();
socket.close();
System.out.println("客户端已退出...");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
代码解读:
Socket(InetAddress address, int port)
: 创建一个 Socket 连接,指定服务端的 IP 地址和端口号。连接成功后,返回一个用于与服务端通信的 Socket 对象。socket.getOutputStream()
: 获取输出流,用于向服务端发送数据。outputStream.write()
: 将数据写入输出流,发送到服务端。
# 1.3 执行过程与控制台输出
- 启动服务端,服务端开始监听指定端口(如 8888),等待客户端连接:
服务端:正在 8888 端口监听,等待客户端连接...
1 - 启动客户端,客户端连接成功后发送消息,连接结束后关闭:
客户端 socket = class java.net.Socket 客户端已退出...
1
2 - 服务端接收到客户端发送的消息并输出:
服务端 socket = class java.net.Socket Hello, SocketTCPServer01! 这是客户端发送的一条消息。
1
2
# 1.4 重要的 API 和参数说明
ServerSocket(int port)
: 创建并绑定到指定端口的服务器套接字。端口号用于标识网络服务,需确保唯一性。Socket(InetAddress address, int port)
: 客户端通过该构造方法连接指定的 IP 地址和端口号。accept()
: 阻塞方法,等待客户端连接成功返回Socket
实例,用于与客户端通信。getInputStream()/getOutputStream()
: 获取输入输出流,分别用于读取和发送数据。write(byte[] b)
: 将字节数组数据写入输出流并发送。close()
: 关闭套接字和相关流,释放系统资源。
# 2. 创建更复杂的客户端-服务端连接
在前面的示例中,我们实现了一个简单的 TCP 通信程序,客户端发送一条消息后即断开连接。现在,我们来改进这个程序,使得客户端在发送完消息后,还可以接收到服务端的回复,再进行断开连接。
需求: 客户端连接到服务端后,发送一条消息;服务端接收到消息后,回发一条消息给客户端,客户端接收后再断开连接。
实现思路:
- 客户端在发送完消息后,通过设置标识来告知服务端消息发送完毕。
- 服务端在接收到消息并确认后,回发一条消息给客户端,并设置标识表示消息发送完成。
- 客户端收到服务端的消息后,断开连接。
# 2.1 改进后的服务端代码详解
package org.javatop.socket;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @description: TCP 服务端示例 - 改进版
* @version: 1.0
* @author: scholar
* @date: 2023-11-02
*/
public class SocketTCPServer02 {
public static void main(String[] args) throws IOException {
/*
1. 创建一个 ServerSocket 对象,绑定到指定端口(如 8888),用于监听客户端连接请求。
- ServerSocket 可以通过 accept() 方法接受多个客户端的连接请求,支持并发。
*/
ServerSocket serverSocket = new ServerSocket(8888);
System.out.println("服务端:正在 8888 端口监听,等待客户端连接...");
// 2. accept() 方法会阻塞当前线程,直到有客户端连接到指定端口。
// 当客户端连接成功时,返回一个 Socket 对象,代表客户端的连接。
Socket socket = serverSocket.accept();
System.out.println("服务端 socket = " + socket.getClass());
// 3. 获取输入流,读取客户端发送的数据。
InputStream is = socket.getInputStream();
byte[] buf = new byte[1024];
int readLen = 0;
while ((readLen = is.read(buf)) != -1) {
// 将接收到的字节数据转换为字符串,并输出到控制台
System.out.println(new String(buf, 0, readLen));
}
// 4. 获取输出流,向客户端发送消息
OutputStream outputStream = socket.getOutputStream();
outputStream.write("hello, SocketTCPClient02! 这是服务端发送的一条消息...".getBytes());
// 5. 设置结束标记,告知客户端消息发送完毕
socket.shutdownOutput();
// 6. 关闭流和 Socket 连接,释放资源
is.close();
socket.close();
serverSocket.close();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
代码解读:
socket.shutdownOutput()
: 该方法用于告诉接收方当前输出流的写操作已完成,通常用于标记消息发送完毕。接收方在读取到流的结束标记后会停止接收数据。
# 2.2 改进后的客户端代码详解
package org.javatop.socket;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
/**
* @description: TCP 客户端示例 - 改进版
* @version: 1.0
* @author: scholar
* @date: 2023-11-02
*/
public class SocketTCPClient02 {
public static void main(String[] args) throws IOException {
/*
1. 创建一个 Socket 对象,连接到服务端的 IP 地址和端口号。
- 参数1: InetAddress 表示服务端的 IP 地址,可以通过 getLocalHost() 获取本机地址。
- 参数2: int 表示服务端的端口号(如 8888)。
*/
Socket socket = new Socket(InetAddress.getLocalHost(), 8888);
System.out.println("客户端 socket = " + socket.getClass());
// 2. 获取输出流,向服务端发送数据
OutputStream outputStream = socket.getOutputStream();
outputStream.write("hello, SocketTCPServer02! 这是客户端发送的一条消息...".getBytes());
// 3. 设置结束标记,告知服务端消息发送完毕
socket.shutdownOutput();
// 4. 获取输入流,读取服务端回发的数据
InputStream inputStream = socket.getInputStream();
byte[] buf = new byte[1024];
int readLen = 0;
while ((readLen = inputStream.read(buf)) != -1) {
System.out.println("客户端收到服务端的回复信息 = " + new String(buf, 0, readLen));
}
// 5. 关闭流和 Socket 连接,释放资源
outputStream.close();
inputStream.close();
socket.close();
System.out.println("客户端已退出...");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
代码解读:
socket.shutdownOutput()
: 该方法在客户端发送完消息后调用,标记输出流已完成,通知服务端可以开始读取数据。- 客户端在接收完服务端的回复后,关闭所有相关流和
Socket
连接。
# 2.3 执行过程与控制台输出
- 服务端启动并监听端口,等待客户端连接:
服务端:正在 8888 端口监听,等待客户端连接...
1 - 客户端连接成功后发送消息,并等待服务端的回复:
客户端 socket = class java.net.Socket 客户端收到服务端的回复信息 = hello, SocketTCPClient02! 这是服务端发送的一条消息... 客户端已退出...
1
2
3 - 服务端接收到客户端消息后回发,并标记消息发送完毕:
服务端 socket = class java.net.Socket hello, SocketTCPServer02! 这是客户端发送的一条消息...
1
2
# 2.4 重要的 API 和参数说明
socket.shutdownOutput()
:用于标记输出流的结束,通知接收方不再有更多数据。当接收方读取到流的结束标记时,会停止读取,避免数据传输不完整。accept()
:服务端阻塞方法,用于等待客户端连接。一旦连接成功,返回一个与客户端通信的Socket
对象。getInputStream()/getOutputStream()
:用于获取输入/输出流,分别用于读取和发送数据。close()
:关闭流和Socket
,释放系统资源。
# 3. TCP 通信的细节
# 1. 构造方法
TCP 通信中的构造方法决定了如何创建服务端和客户端的 Socket
实例。以下是常用的构造方法:
方法名 | 说明 |
---|---|
ServerSocket(int port) | 创建一个绑定到指定端口的服务器套接字,用于监听客户端连接 |
说明:
ServerSocket(int port)
:此构造方法用于在指定的端口上创建一个ServerSocket
实例,服务端通过这个套接字监听客户端的连接请求。端口号必须唯一,且未被其他服务占用。
# 2. 相关方法
在实际通信过程中,服务端和客户端需要使用以下关键方法进行连接和数据传输:
方法名 | 说明 |
---|---|
Socket accept() | 监听连接请求并接受与客户端的连接,返回一个 Socket 对象 |
说明:
accept()
:该方法用于服务端监听客户端的连接请求。调用此方法时,程序会进入阻塞状态,直到有客户端连接成功,返回一个与客户端通信的Socket
对象。
# 3. 注意事项
在进行 TCP 通信编程时,需要注意以下几点:
accept()
方法是阻塞的:服务端在调用accept()
方法时会一直等待,直到有客户端连接成功。这种阻塞是必要的,以确保客户端连接的稳定性。客户端连接服务器的过程使用了三次握手协议:三次握手过程确保客户端和服务器之间的连接可靠性。这个过程建立了连接,使得双方可以开始通信。
数据流的方向性:
- 对于客户端来说,它发送数据,因此使用的是 输出流(OutputStream)。
- 对于服务端来说,它接收数据,因此使用的是 输入流(InputStream)。
read()
方法也是阻塞的:在读取数据时,read()
方法会阻塞线程,直到有数据可读或连接关闭。这种阻塞有助于确保数据接收的完整性。客户端关闭连接时会发送结束标记:在客户端关闭输出流时,会自动发送一个结束标记(EOF),通知服务端数据已发送完毕。这一点在双向通信中尤为重要。
连接的关闭通过四次挥手协议完成:四次挥手过程确保连接被安全终止,避免数据丢失或传输中断。
# 4. TCP 三次握手详解
三次握手(Three-Way Handshake) 是建立 TCP 连接的过程,确保连接的可靠性。具体步骤如下:
- 第一次握手:客户端发送一个
SYN
标志位的数据包给服务端,表示请求建立连接。 - 第二次握手:服务端收到请求后,向客户端回传一个带有
SYN/ACK
标志位的数据包,表示同意建立连接,并请求客户端进行确认。 - 第三次握手:客户端收到服务端的
SYN/ACK
包后,再次回传一个带有ACK
标志位的数据包,表示确认连接建立成功。
至此,客户端和服务端的连接成功建立,双方可以开始数据传输。
# 5. TCP 四次挥手详解
四次挥手(Four-Way Handshake) 是终止 TCP 连接的过程,确保数据传输的完整性。具体步骤如下:
- 第一次挥手:客户端发送一个
FIN
标志位的数据包,表示它已经没有数据要发送,并请求关闭连接。 - 第二次挥手:服务端收到
FIN
后,回传一个带有ACK
标志位的数据包,表示确认关闭请求。此时客户端进入FIN_WAIT_2
状态,等待服务端的关闭。 - 第三次挥手:服务端发送一个
FIN
标志位的数据包,表示它的数据已经发送完毕,准备关闭连接。 - 第四次挥手:客户端收到服务端的
FIN
后,回传一个带有ACK
标志位的数据包,表示确认关闭。此时,客户端和服务端都进入CLOSED
状态,连接正式关闭。
总结
- 三次握手 确保在通信开始前,客户端和服务端的通信链路是可靠且可用的。
- 四次挥手 确保在通信结束时,双方的数据都已传输完成,不会出现数据丢失或中断。