在学习 Java 网络编程时,我们会接触 IP 地址、端口号、TCP、UDP、Socket、输入流和输出流等概念。如果只看定义,这些知识很容易显得抽象。
这些定义的详细解释在我上上篇博客中其实已经进行了比较详细的解释,如果还不是很了解的小伙伴可以移步这篇文章计算机网络基础:TCP、UDP、IP、端口号与套接字先进行了解,我也会在文章的第一节对这些基础知识进行简单的解释。
本文将从一个具体目标出发:编写两个独立的 Java 程序,让它们建立 TCP 连接,并在控制台中互相发送消息。这个程序在后续也会不断的完善,在此过程中将持续更新博客,感兴趣的话可以持续关注!
一、网络编程基础概念
1.1 什么是网络编程
网络编程就是让不同的程序通过网络交换数据。这些程序可以在不同的计算机上,也可以像文本一样,先在同一台计算机上进行测试。
一次网络通信至少需要解决三个问题
- 对方在哪台计算机上?
- 数据应该交给这台计算机中的哪个程序?
- 双方按照什么规则传输和解释数据?
对应的基础概念分别是:
- IP 地址:定位计算机;
- 端口号:定位计算机中的程序;
- 网络协议:规定双方通信的规则。
1.2 什么是输入流和输出流?
连接建立以后,双方通过流收发数据:
InputStream + read() = 接收数据 OutputStream + write() = 发送数据输入和输出是站在当前程序的角度定义的:
数据进入当前程序 = 输入 数据离开当前程序 = 输出所以两边流的对应关系是:
A 的 OutputStream → B 的 InputStream B 的 OutputStream → A 的 InputStream二、从零实现两个 Java 程序的 TCP 对话
前面已经介绍过 IP、端口号、TCP、Socket 等基础概念。现在开始进行代码的实现过程。
首先,我们明确最终要实现的效果为:
A 程序输入消息 ↓ A 将消息发送给 B ↓ B 在控制台打印消息 ↓ B 输入回复 ↓ B 将回复发送给 A ↓ A 在控制台打印回复在进行下面步骤之前首先要在Maven下创建项目,然后创建两个类,在这里为了方便表示直接将他们命名为A.java和B.java
2.1 第一步:让 B 监听端口并等待 A 连接
2.1.1 创建 ServerSocket 并监听 54321 端口
在前面的流程中,B 的第一个职责是等待 A 的连接。因此,我们先在B.java的main方法中创建一个ServerSocket,相关的代码如下所示:
此行代码就表示,当B程序启动之后就会在本机的54321端口上创建了一个服务端。我们可以把它理解为 B 先在本机创建了一个“接待点”,地址为:127.0.0.1:54321 。之后,A 程序只要连接这个地址,就能找到 B。
但是我们也注意到了,写完这行代码之后代码中有红色下划线提示错误,这个时候我们需要用try\catch来解决这个问题(快捷键为Alt + Enter,光标放在画线单词上)
同时需要对变量ss进行初始化,解决后的完整代码如下所示
public class B { public static void main(String[] args) { ServerSocket ss = null; try { ss = new ServerSocket(54321); } catch (IOException e) { throw new RuntimeException(e); } } }2.1.2 调用accept( )等待A的连接
此处的问题与上面红线的问题一致,解决方法也一致,不多赘述了,解决之后的代码如下所示
try { Socket s = ss.accept(); } catch (IOException e) { throw new RuntimeException(e); }2.2 第二步: 让A主动连接B
B 负责等待,A 则负责主动发起连接。相关的代码为
Socket sc = new Socket("127.0.0.1", 54321);这行代码的含义是:A 程序尝试连接到:IP 地址为127.0.0.1 , 端口号为 54321 的程序。由于本例中 A 和 B 都运行在同一台电脑上,所以使用:127.0.0.1 ,他表示本机。
用try\catch解决上述问题后相关代码如下:
public class A { public static void main(String[] args) { try { Socket sc = new Socket("127.0.0.1", 54321); } catch (IOException e) { throw new RuntimeException(e); } } }此时,A 和 B 已经建立 TCP 连接,不过,现在只是连接成功,双方还没有发送任何消息。
现在我们可以测试一下,在两段代码中都加上对应的输出语句,注意是要先运行B!因为这样才能有对应的端口让A去连接。我就不在此赘述了。
2.3 第三步: 让A获取输入流、输出流和控制台输入
A 连接成功后,接下来要完成两件事:
- 从控制台获取用户输入的消息;
- 将消息发送给 B。
首先,需要让A能够读取来自B的消息,我们用输入流来实现,相关的代码如下所示(当然啦,这个功能需要在try里面实现哦)代码的意思为:A 通过in读取 B 发来的消息。
InputStream in = sc.getInputStream();其次我们要让A能给B发去信息,我们用输出流来实现,相关的代码如下(同上注意事项)代码的意思为:A 通过os向 B 发送消息。
OutputStream os = sc.getOutputStream();我们还需要让A能够读取控制台中输入的内容,相关的代码如下所示。代码的意思为:A 通过scanner读取控制台中输入的内容。
Scanner scanner = new Scanner(System.in);此时A的状态可以表示为:
控制台输入 ↓ Scanner scanner ↓ A 程序 ↓ OutputStream os ↓ B 程序 B 的回复 ↓ InputStream in ↓ A 程序 ↓ 控制台输出接下来,我们先实现A从控制台输入一句话。
2.3.1 让 A 能够持续读取控制台输入
现在 A 已经拥有:
in:读取 B 发来的消息;os:向 B 发送消息;scanner:读取控制台输入。
接下来,我们需要让 A 在控制台中输入要发送给 B 的内容。由于后面需要不断进行对话,所以先使用while (true)将整个聊天过程放入循环中:
while (true) { System.out.println("请说一个字:"); String m = scanner.nextLine();代码的意思是:A 每次先在控制台输出“请说一个字:”,然后通过scanner读取用户输入的一行内容,并保存到变量m中。
现在 A 已经可以读取控制台输入,但消息还没有发送给 B。接下来需要通过输出流os将消息发送出去。
2.3.2 A 先发送消息的长度
在发送消息内容之前,A需要先把消息的长度发送给B。这样做是为了让B在读取消息的时候知道接下来需要读取多少字符!
相关的代码为:(写在刚刚的while循环中)
int len = m.length(); os.write(len);现在A 和 B的通信格式为:先发送消息长度,再发送消息内容。
2.3.3 A 发送消息内容
在while循环中继续添加一个for循环用于向B发送信息
for (int i = 0; i < len; i++) { os.write(m.charAt(i)); } os.flush();这段代码会依次取出m中的每一个字符,并通过输出流发送给 B。
os.flush(); 表示将已经写入输出流的数据发送出去。
至此,A 已经完成了发送消息的操作。接下来,A 需要等待 B 的回复。
2.3.4 A 等待并读取 B 的回复
B在回复消息的时候同样会先发送消息的长度,再发送消息内容。因此,A在接受消息的时候需要先读取长度,相关的代码如下:
int len1 = in.read();// 等待 阻塞 System.out.println("收到消息:");当 B 还没有发送回复时,程序会停在:
in.read();这一行等待消息的到来。这也就是注释中“等待 阻塞”的含义:当前线程暂时不继续执行,直到 B 向 A 发送数据。
当 A 读取到 B 发送的消息长度后,接下来按照这个长度读取每一个字符:
for (int i = 0; i < len1; i++) { int m1 = in.read(); System.out.print((char) m1); }代码的意思是:循环读取len1个字符,并将每一个字符打印到 A 的控制台中。
这样,A 的一次操作就完成了:
A 输入消息 ↓ A 将消息发送给 B ↓ A 等待 B 回复 ↓ A 打印 B 的回复由于这些代码写在while (true)中,因此 A 收到回复后,会再次回到:
System.out.println("请说一个字:");继续等待下一次输入。
此时 A 中循环部分的完整代码如下:
while (true) { System.out.println("请说一个字:"); String m = scanner.nextLine(); // 发送的第一个字节是数据的长度 int len = m.length(); os.write(len); for (int i = 0; i < len; i++) { os.write(m.charAt(i)); } os.flush(); int len1 = in.read();// 等待 阻塞 System.out.println("收到消息:"); for (int i = 0; i < len1; i++) { int m1 = in.read(); System.out.print((char) m1); } System.out.println(); }A 的代码暂时写到这里。下面回到 B,让 B 能够接收 A 发来的消息。
2.4 第四步:让 B 接收 A 的消息并回复
B 与 A 建立连接后,同样需要获取输入流、输出流和控制台输入工具,相关代码如下:
// 对话 InputStream in = s.getInputStream();// 输入流 读取对方的消息 OutputStream os = s.getOutputStream();// 输出流 给对方发消息 Scanner scanner = new Scanner(System.in);接下来同样使用循环,让 B 可以持续接收消息并回复:
while (true) {B 和 A 的执行顺序不同:
A:先发送消息,再等待回复 B:先接收消息,再输入回复因此,B 进入循环后的第一件事,是读取 A 发来的消息。
2.4.1 B 读取 A 发送的消息长度
A 发送消息时,第一个发送的数据是消息长度:
os.write(len);所以 B 需要先读取消息长度:
int len = in.read(); System.out.println("收到消息:");例如,A 输入:
helloA 首先发送长度5,B 执行:
int len = in.read();后,len的值就是5。
接下来,B 就知道需要继续读取 5 个字符。
2.4.2 B 读取并打印 A 的消息内容
继续添加以下代码:
for (int i = 0; i < len; i++) { int m1 = in.read();// 接受消息 System.out.print((char) m1);// 转成字符打印 } System.out.println();这段代码会按照刚才读取到的长度,逐个读取 A 发送的字符。
例如,A 发送:
helloB 的控制台会输出:
收到消息: hello到这里,B 已经可以接收并打印 A 发来的消息。 接下来,让 B 在控制台输入回复。
2.4.3 B 从控制台读取回复
在 B 打印完 A 的消息后,添加以下代码:
System.out.println("请输入消息:"); String m = scanner.nextLine();B 的控制台会显示:
请输入消息:随后等待用户输入。
例如,B 输入:
你好那么这段内容会保存到变量m中。
接下来,B 需要把这段回复发送给 A。
2.4.4 B 将回复发送给 A
B 发送消息的方式与 A 相同:先发送长度,再发送每一个字符。
相关代码如下:
int len1 = m.length(); os.write(len1); for (int i = 0; i < len1; i++) { os.write(m.charAt(i));// 写字到输入框中 } os.flush();// 点击发送按钮这里:
int len1 = m.length();用于获取 B 回复消息的长度。
os.write(len1);先将长度发送给 A。
随后通过循环,将回复中的每一个字符发送出去:
for (int i = 0; i < len1; i++) { os.write(m.charAt(i)); }最后:
os.flush();将消息立即发送给 A。
此时,B 中循环部分的完整代码如下:
while (true) { int len = in.read(); System.out.println("收到消息:"); for (int i = 0; i < len; i++) { int m1 = in.read();// 接受消息 System.out.print((char) m1);// 转成字符打印 } System.out.println(); System.out.println("请输入消息:"); String m = scanner.nextLine(); int len1 = m.length(); os.write(len1); for (int i = 0; i < len1; i++) { os.write(m.charAt(i));// 写字到输入框中 } os.flush();// 点击发送按钮 }至此,A 和 B 就可以按照固定顺序持续对话:
A 输入消息 ↓ A 发送消息给 B ↓ B 接收并打印消息 ↓ B 输入回复 ↓ B 发送回复给 A ↓ A 接收并打印回复 ↓ 进入下一轮对话整个通信过程始终遵循下面的顺序:
A:发送 → 接收 B:接收 → 发送因此,运行程序时仍然需要先启动 B,再启动 A。
这样一个基础的通过TCP 对话对话的程序我们就实现了,接下来会持续更新哦,并且会进一步完善!!
完整代码如下
A.java
import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.util.Scanner; // 网络基础知识 // 实现两个程序之间发送消息 public class A { public static void main(String[] args) { // 套接字 try { Socket sc = new Socket("127.0.0.1", 54321);// 男方 System.out.println("发起连接~"); InputStream in = sc.getInputStream(); OutputStream os = sc.getOutputStream(); Scanner scanner = new Scanner(System.in); while (true) { System.out.println("请说一个字:"); String m = scanner.nextLine(); // 发送的第一个字节是数据的长度 int len = m.length(); os.write(len); for (int i = 0; i < len; i++) { os.write(m.charAt(i)); } os.flush(); int len1 = in.read();// 等待 阻塞 System.out.println("收到消息:"); for (int i = 0; i < len1; i++) { int m1 = in.read(); System.out.print((char) m1); } System.out.println(); } } catch (IOException e) { throw new RuntimeException(e); } } }B.java
import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; import java.util.Scanner; public class B { public static void main(String[] args) { // 套接字的服务类 // 54321 女方家门牌号 ServerSocket ss = null;// 红娘 try { ss = new ServerSocket(54321); } catch (IOException e) { throw new RuntimeException(e); } try { System.out.println("等待A来连接..."); Socket s = ss.accept(); System.out.println("连接成功~"); // 对话 InputStream in = s.getInputStream();// 输入流 读取对方的消息 OutputStream os = s.getOutputStream();// 输出流 给对方发消息 Scanner scanner = new Scanner(System.in); while (true) { int len = in.read(); System.out.println("收到消息:"); for (int i = 0; i < len; i++) { int m1 = in.read();// 接受消息 System.out.print((char) m1);// 转成字符打印 } System.out.println(); System.out.println("请输入消息:"); String m = scanner.nextLine(); int len1 = m.length(); os.write(len1); for (int i = 0; i < len1; i++) { os.write(m.charAt(i));// 写字到输入框中 } os.flush();// 点击发送按钮 } } catch (IOException e) { throw new RuntimeException(e); } } }