文章目录
前言
📕各位读者好, 我是小陈, 这是我的个人主页
📗小陈还在持续努力学习编程, 努力通过博客输出所学知识
📘如果本篇对你有帮助, 烦请点赞关注支持一波, 感激不尽
📙 希望我的专栏能够帮助到你:
JavaSE基础: 基础语法, 类和对象, 封装继承多态, 接口, 综合小练习图书管理系统等
Java数据结构: 顺序表, 链表, 堆, 二叉树, 二叉搜索树, 哈希表等
JavaEE初阶: 多线程, 网络编程, TCP/IP协议, HTTP协议, Tomcat, Servlet, Linux, JVM等(正在持续更新)
上篇文章介绍了基于 UDP 协议的 Socket API, 以及简单写了一个回显服务器, 实现了服务器和客户端之间的网络通信
本篇将介绍网络编程中 : 基于 TCP 协议的 Socket 套接字的相关知识
提示:是正在努力进步的小菜鸟一只,如有大佬发现文章欠佳之处欢迎批评指点~ 废话不多说,直接上干货!
一、认识 Socket(套接字), TCP 协议和 UDP 协议
以下内容上篇介绍过了, 看过上篇文章的读者可以跳过
上篇提到, 我们程序员进行网络编程主要是在 TCP/IP 五层网络模型中的应用层, 而数据在网络上传输, 需要进行封装和分用, 其中应用层需要调用传输层提供的 API , 这一组 API 就被称作 Socket API
1, 什么是 Socket(套接字)
概念 : Socket 套接字是由系统提供于网络通信的技术, 是基于 TCP/IP 协议的网络通信的基本操作党员, 基于 Socket 套接字的网络程序开发就是网络编程
要进行网络通信, 需要有一个 socket 对象, 一个 socket 对象对应着一个 socket 文件, 这个文件在 网卡上而不是硬盘上, 所以有了 sokcet 对象才能通过操作内存来操作网卡
在 socket 文件中写数据相当于通过网卡发送数据, 在 socket 文件中读数据相当于通过网卡接收数据
Socket API 分为两类 : 基于 TCP 协议的 API , 和基于 UDP 协议的 API, 下面先认识一下 TCP 协议和 UDP 协议的区别和特点
2, 浅谈 TCP 协议和 UDP 协议的区别和特点
TCP 协议 | 说明 | UDP 协议 | 说明 |
---|---|---|---|
有连接 | 通信双方需要刻意保存对方的相关信息 | 无链接 | 通信双方不需要刻意保存对方的信息 |
可靠传输 | 如果数据发送不成功, 发送方会知道 | 不可靠传输 | 发送方不关心数据是否发送成功 |
面向字节流 | 发送的数据以字节为单位 | 面向数据报 | 发送的数据以 UDP 数据报为单位 |
全双工 | 双向通信 | 全双工 | 双向通信 |
这里只做简单介绍, 这两个协议后续会单独详细介绍
二、基于 TCP 协议的 Socket API
首先要明确 TCP 协议和 UDP 协议的很重要的区别 : TCP 协议是有链接, 面向字节流传输, 主要体现在 : 发送方和接收方在网络通信之间要先建立连接, 并且传输的数据的基本单位是字节
基于 TCP 协议的 Socket API 中, 要分清楚以下两个类 :
类名 | 解释 |
---|---|
ServerSocket | 只能服务器使用, 客户端不能使用, 这个类是在等待客户端发起连接之前不做任何事的"监听器" |
Socket | 服务器或客户端都可以使用, 客户端使用这个类向服务器发起连接之后, 双端都使用这个类进行网络通信 |
这两个类的联系就是, 服务器启动之后, 先使用 ServerSocket 类等待客户端发来连接请求, 连接成功后服务器和客户端都使用 Socket 类进行通信
1, ServerSocket 类
ServerSocket 类的构造方法 :
方法签名 | 作用 |
---|---|
ServerSocket (int port) | 创建一个 ServerSocket 对象, 一般用于服务器, 需要指定本机端口号 |
ServerSocket 类的成员方法 :
方法签名 | 作用 |
---|---|
Socket accept() | 开始"监听", 有客户端发来连接请求之后, 返回一个用于服务器使用的 Socket 对象, 如果客户端没有发起连接, 则阻塞等待 |
void close() | 关闭 ServerSocket |
2, Socket 类
再次说明, Socket 这个类用于客户端, 也可以在服务器与客户端连接之后使用, 无论客户端或服务器使用, 都会保存对端的相关信息
Socket 类的构造方法 :
方法签名 | 作用 |
---|---|
Socket(String host, int port) | 一般用于客户端, 需要指定服务器的 IP 地址和端口号 |
void close() | 用于关闭 ServerSocket |
Socket 类的成员方法 :
由于 TCP 协议是面向字节流, 所以有两个关于字节流输入输出的成员方法
方法签名 | 作用 |
---|---|
InputStream getInputStream() | 获取 Socket 的字节输入流 |
OutputStream getOutputStream() | 获取 Socket 的字节输出流 |
InetAddress getInetAddress() | 获取对端的 IP 地址 |
InetAddress getPort() | 获取对端的端口号 |
调用 getInputStream() 和 getOutputStream() 这个两个方法, 就可以通过字节流对象, 从网卡中读写数据
getInputStream()返回的对象用来输入(读), 从网卡读数据到内存(从网卡接收数据)
getOutputStream返回的对象用来输出(写), 从网卡写数据到内存(从网卡发送数据)
先对上述 API 有个印象即可, 接下来逐行解析如何从 0 到 1 地进行客户端和服务器之间地网络编程, 代码敲完之后再消化吸收
三、逐行代码解析网络编程
下面我们还是写一个最简单的客户端服务器网络通信模型 : 客户端给服务器发送什么请求, 服务器就给客户发送什么响应(这是最简单但是毫无意义的回显服务器, 只是方便熟悉 TCP Socket 的 API 使用)
客户端和服务器各自为一个进程在运行, 双方互不干涉(当然我们现在要写的客户端服务器程序是在同一台主机上的)
一定是服务器先启动, 一直等待客户端发来请求, 所以按照时间顺序, 代码逻辑应该如下所示 :
客户端 | 服务器 |
---|---|
/ | 1, 启动服务器, 构造 ServerSocket 对象, 调用 accept() 时刻准备和客户端连接 |
2, 构造 Socket 对象即为发起连接 | / |
/ | 3, 连接成功, 通过 accept() 的返回值得到 Socket 对象 |
4, 把请求写入网卡 | |
/ | 5, 从网卡读取请求 |
/ | 6, 处理请求 |
/ | 7, 把响应写入网卡 |
8, 从网卡读取响应 | / |
有了这个思路, 下面正式开始使用上述 API 进行网络编程
1, 逐行解析客户端
创建一个类 : TCPEchoClient 作为客户端
成员属性 :
需要定义一个 Scoket 对象来进行和服务器的通信
public class TCPEchoClient {
// 成员属性
private Socket socket = null;
}
构造方法 :
用于实例化客户端的 socket 对象, 别忘了需要绑定服务器的 IP 地址和端口号
public class TCPEchoClient {
// 成员属性
private Socket socket = null;
// 构造方法
public TCPEchoClient(String serverIP, int serverPort) throws IOException {
socket = new Socket(serverIP, serverPort);
}
}
main 方法 :
1, 构造 tcpEchoClient 对象, 由于服务器在本机, IP 地址为"127.0.0.1", 端口号随意指定 [1024, 65535] 之间的任意一个
2, 调用 TCPEchoClient 类的核心成员方法 start(), 这个方法实现了客户端的核心逻辑
public class TCPEchoClient {
// 成员属性
private Socket socket = null;
// 构造方法
public TCPEchoClient(String serverIP, int serverPort) throws IOException {
socket = new Socket(serverIP, serverPort);
}
// main 方法
public static void main(String[] args) throws IOException {
TCPEchoClient tcpEchoClient = new TCPEchoClient("127.0.0.1", 9999);
tcpEchoClient.start();
}
}
1.1, 核心成员方法 start()
1️⃣构造一个 Scanner 对象, 从控制台输入字符串, 这个字符串当作请求的内容
2️⃣核心逻辑在一个 while(true) 循环中, 实现多次发送请求
public void start() {
Scanner in = new Scanner(System.in);
// 发送多个请求
while (true) {
}
}
}
由于TCP协议是面向字节流传输, 所以为了方便读写数据, 我们把字节流转化成字符流处理
所以在进入 while 循环之前, 先构造字符流的输入输出对象
public void start() {
Scanner in = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
// 把字节流转换成字符流
PrintWriter printWriter = new PrintWriter(outputStream);
Scanner inFromSocket = new Scanner(inputStream);
// 发送多个请求
while (true) {
}
} catch (IOException e) {
e.printStackTrace();
}
}
getInputStream()返回的对象用来输入(读), 从网卡读数据到内存(从网卡接收数据)
getOutputStream返回的对象用来输出(写), 从网卡写数据到内存(从网卡发送数据)
然后每次进入循环, 主要有两个操作 : 1, 把请求写入网卡 2, 把响应从网卡中读出来, 写的使用调用 println(), 读的时候调用 next(), 这样能以空白符为结束标志进行读写数据
图解如下 :
2, 逐行解析服务器
创建一个类 TCPEchoServer 作为服务器
成员属性 :
需要定义一个 ServerSocket 对象, 用来等待客户端发来连接的"监听器"
public class TCPEchoServer {
// 构造方法
private ServerSocket serverSocket = null;
}
构造方法 :
用于实例化客户端的 ServerSocket 对象, 别忘了需要绑定本机端口号
public class TCPEchoServer {
// 构造方法
private ServerSocket serverSocket = null;
// 构造方法
public TCPEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
}
main 方法 :
1, 构造 tcpEchoServer 对象, 需要绑定端口号, 必须和客户端那边绑定的一致
2, 调用 tcpEchoServer 类的核心成员方法 start(), 这个方法实现了服务器的核心逻辑
public class TCPEchoServer {
// 构造方法
private ServerSocket serverSocket = null;
// 构造方法
public TCPEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
// main 方法
public static void main(String[] args) throws IOException {
TCPEchoServer tcpEchoServer = new TCPEchoServer(9999);
tcpEchoServer.start();
}
}
2.1, 核心成员方法 start()
由于 TCP 是有连接的传输协议, 所以服务器在和客户端连接之前, 要先和客户端建立连接, 也就是调用 accept(), 连接成功之后, 服务器就可以处理这个连接了
如果有多个服务器来和客户端连接, 服务器就需要处理多个连接, 所以把上述过程写在 while(true) 中
public void start() throws IOException {
while (true) {
// 建立连接 返回一个 Socket 对象
Socket socket = serverSocket.accept();
// 处理连接到的这个客户端
processConnection(socket);
}
}
处理连接的过程其实就是从网卡中读取数据, 处理响应, 再把响应写回网卡, 我们把这个过程封装成 processConnection(Socket socket);
注意 : 调用 accept() 的是 ServerSocket 的对象, 而这个方法的返回值是Socket, 上面已经强调过了
接下来解析 processConnection() 的过程
和客户端一样, 要先把字节流转化成字符流, 方便读写数据
private void processConnection(Socket socket) {
System.out.println(socket.getInetAddress() + " + " + socket.getPort() + " + " + "此客户端上线");
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream() ) {
PrintWriter printWriter = new PrintWriter(outputStream);
Scanner inFromSocket = new Scanner(inputStream);
// 处理多个请求
while(true) {
}
} catch (IOException e) {
e.printStackTrace();
}
}
getInputStream()返回的对象用来输入(读), 从网卡读数据到内存(从网卡接收数据)
getOutputStream返回的对象用来输出(写), 从网卡写数据到内存(从网卡发送数据)
然后就是while循环, 进入循环后主要就三个操作: 1, 从网卡中读取数据 2, 处理响应 3, 再把响应写回网卡
图解如下 :
看到这里, 应该感受到了 TCP 和 UDP 的不同之处体现在哪了
首先是 TCP 的服务器需要先使用 ServerSocket 建立连接, 建立连接之后服务器和客户端都是用 Socket 进行通信
通信时, TCP 进行传输使用的是字节流, 直接从网卡读写, 但我们可以转化成字符流, 而 UDP 进行传输是把数据封装成 DatagramPacket(数据报), 再进行发送和接收
3, bug 修改
3.1, bug1
上述代码中, 有个隐性的严重的 bug, 由于服务器可能是处理多个客户端连接, 那么处理完客户端 A 后, 服务器这个进程不一定会结束, 很有可能还要处理客户端 B
所以服务器和某个客户端进行通信时打开的 Socket 文件就必须在 finally 语句块中调用 close(), 以避免内存资源泄露, 修改后的代码如下 :
private void processConnection(Socket socket) throws IOException {
System.out.println(socket.getInetAddress() + " + " + socket.getPort() + " + " + "此客户端上线");
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream() ) {
// 处理多个请求
while(true) {
}
} catch (IOException e) {
e.printStackTrace();
} finally {
socket.close();
}
}
客户端那边不需要调用 close() 是因为在当前场景下, 客户端的 Socket 生命争取伴随着整个客户端进程, 不会出现频繁创建 Socket 但没有 close 导致内存资源泄露
3.2, bug2
还有一个显性的 bug, 我们首先打开两个客户端, 步骤如下 :
然后先运行服务器, 再运行两个客户端, 观察运行效果 :
1 号客户端 :
2 号客户端 :
服务器 :
会发现, 第二个开启的客户端并没有和服务器成功通信, 这是因为, 我们的服务器处理多个连接时, 是在一个while循环中, 如果第一个连接的客户端没有下线, 就不会接收第二个客户端的连接
public void start() throws IOException {
while (true) {
// 建立连接 返回一个 Socket 对象
Socket socket = serverSocket.accept();
// 处理连接到的这个客户端
processConnection(socket);
}
}
正确的代码应该是, 每连接成功一个客户端, 就开启一个线程来处理这个连接, 修改后的代码如下 :
public void start() throws IOException {
while (true) {
// 建立连接 返回一个 Socket 对象
Socket socket = serverSocket.accept();
// 处理连接到的这个客户端
Thread thread = new Thread( () -> {
try {
processConnection(socket);
} catch (IOException e) {
e.printStackTrace();
}
});
// 别忘了调用 start() 启动线程
thread.start();
}
}
3.3, 最终运行效果
1 号客户端 :
2 号客户端 :
服务器 :
四、完整代码
1, 客户端
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
public class TCPEchoClient {
private Socket socket = null;
public TCPEchoClient(String serverIP, int serverPort) throws IOException {
socket = new Socket(serverIP, serverPort);
}
public void start() {
Scanner in = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
// 把字节流转换成字符流
PrintWriter printWriter = new PrintWriter(outputStream);
Scanner inFromSocket = new Scanner(inputStream);
// 发送多个请求
while (true) {
// 1,从控制台输入字符串
String requestString = in.next();
// 2,写入请求
printWriter.println(requestString);
printWriter.flush();
// 3,读取请求
String responseString = inFromSocket.next();
// 控制台 打印请求字符串 + 响应字符串
System.out.println(requestString + " + " + responseString);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TCPEchoClient tcpEchoClient = new TCPEchoClient("127.0.0.1", 9999);
tcpEchoClient.start();
}
}
2, 服务器
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class TCPEchoServer {
private ServerSocket serverSocket = null;
public TCPEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
while (true) {
// 建立连接 返回一个 Socket 对象
Socket socket = serverSocket.accept();
// 处理连接到的这个客户端
Thread thread = new Thread( () -> {
try {
processConnection(socket);
} catch (IOException e) {
e.printStackTrace();
}
});
// 别忘了调用 start() 启动线程
thread.start();
}
}
private void processConnection(Socket socket) throws IOException {
System.out.println(socket.getInetAddress() + " + " + socket.getPort() + " + " + "此客户端上线");
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream() ) {
PrintWriter printWriter = new PrintWriter(outputStream);
Scanner inFromSocket = new Scanner(inputStream);
// 处理多个请求
while(true) {
if (!inFromSocket.hasNext()) {
System.out.println(socket.getInetAddress() + " + " + socket.getPort() + " + " + "此客户端下线");
break;
}
// 1,读取请求
String requestString = inFromSocket.next();
// 2,处理请求
String responseString = process(requestString);
// 3,写入响应
printWriter.println(responseString);
printWriter.flush();
// 控制台打印 客户端IP地址 + 客户端端口号 + 请求字符串 + 响应字符串
System.out.println(socket.getInetAddress() + " + " + socket.getPort() + " + " + requestString + " + " + responseString);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
socket.close();
}
}
private String process(String requestString) {
return requestString;
}
public static void main(String[] args) throws IOException {
TCPEchoServer tcpEchoServer = new TCPEchoServer(9999);
tcpEchoServer.start();
}
}
总结
以上就是本篇的全部内容, 主要介绍了 : 基于 TCP协议的 Socket API , 以及利用这些 API 写了一个最简单但无意义的客户端服务器网络通信程序
再回顾一下, Socket 类的成员方法 :
由于TCP协议是面向字节流, 所以有两个成员方法是关于字节流输入输出的
方法签名 | 作用 |
---|---|
InputStream getInputStream() | 获取 Socket 的字节输入流 |
OutputStream getOutputStream() | 获取 Socket 的字节输出流 |
InetAddress getInetAddress() | 获取对端的 IP 地址 |
InetAddress getPort() | 获取对端的端口号 |
如果本篇对你有帮助,请点赞收藏支持一下,小手一抖就是对作者莫大的鼓励啦😋😋😋~
上山总比下山辛苦
下篇文章见