1、基础简介
TCP协议是面向连接的、可靠的,在数据传输之前建立连接,以此保证数据的可靠性;其次,TCP协议是有序的,并且以字节流的形式发送数据。在Java中基于TCP协议实现网络通讯的类有两个,分别是用于客户端的Socket类及用于服务器端的ServerSocket类,Socket通讯模型如下:
两台主机若想实现通讯,就必然存在着一台作为服务器端(Server),一台作为客户端(Client),首先在服务器端建立一个ServerSocket,并且绑定相应的端口进行监听,等待客户端的连接;之后在客户端创建Socket并向服务器端发送请求,此时服务器端收到客户端发送的请求,并创建连接Socket用于与客户端的Socket进行通信;通信的过程就是借助InputStream以及OutputStream实现数据的发送、接收、响应等等,按照相关的协议对Socket进行读与写的操作,在通信结束后需要关闭双方的Socket及相关资源,即输入与输出流等,以此断开通信,以上便是基于TCP协议的Socket通讯进行的整个过程。
2、通过编程实现“用户登录功能”之客户端
实现用户登录,实际上就是用户信息,例如用户名及密码,从客户端向服务器端发送,被服务器端接收后,再向客户端进行响应,例如回复欢迎登陆等信息的过程。
创建客户端的具体步骤:
- 创建Socket对象,指明需要连接的服务器地址和端口号
- 建立连接后,通过输出流向服务器端发送请求信息
- 通过输入流获取服务器端响应的信息
- 关闭相关资源
示例代码如下:
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
public class Client {
public static void main(String[] args) {
try {
/*
* 创建客户端Socket,指定服务器地址和端口
* 可以理解为客户端与服务器端建立一条通道
* 该通道就是Socket流,也就是客户端对象
* Socket流中既有字节输入流,也有字节输出流
* 本案例中,服务器端和客户端都在本地同一台主机中
* 因此服务器地址可填“127.0.0.1”或“localhost”
* 此时会出现异常,使用try-catch块捕获该异常
*/
Socket socket = new Socket("localhost", 2333);
// 获取输出流,向服务器端发送信息
OutputStream os = socket.getOutputStream();
// 将字节输出流包装为打印流
PrintWriter pw = new PrintWriter(os);
// 编写要向服务器端发送的信息
pw.write("用户名:admin;密码:123456");
// 通过刷新实现发送
pw.flush();
// 禁用输出流
socket.shutdownOutput();
// 关闭相关资源
pw.close();
os.close();
socket.close();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
3、通过编程实现“用户登录功能”之服务器端
创建服务器端的具体步骤:
- 创建ServerSocket对象,并绑定监听端口
- 调用accept()方法进行监听,等待客户端的连接请求
- 与客户端建立连接后,通过输入流读取客户端发送的请求信息
- 通过输出流向客户端发送响应信息
- 关闭相关资源
示例代码如下:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) {
try {
/*
* 创建服务器端Socket,即ServerSocket,并绑定监听端口
* 指定端口时,默认指定1023之后的端口号
* 此时会出现异常,使用try-catch块捕获该异常
*/
ServerSocket serverSocket = new ServerSocket(2333);
/*
* 调用accept()方法进行监听,等待客户端的连接
* 实际上就是获取之前创建的客户端Socket对象
* 这就保证了服务器端与客户端使用的是同一个Socket流
* 为了便于查看结果,添加一句输出
*/
System.out.println("****服务期即将启动,正在等待客户端连接****");
// 一旦调用该方法,服务器端就会进入阻塞状态,等待客户端连接
Socket socket = serverSocket.accept();
// 获取输入流,读取客户端信息
InputStream is = socket.getInputStream();
// 将字节输入流转换为字符输入流
InputStreamReader isr = new InputStreamReader(is);
// 为字符输入流添加缓冲
BufferedReader br = new BufferedReader(isr);
String info = null;
// 循环读取客户端信息
while ((info = br.readLine()) != null) {
System.out.println("服务器端——读取到客户端提交如下信息:" + info);
}
// 将输入流置于末尾
socket.shutdownInput();
// 关闭相关资源
br.close();
is.close();
socket.close();
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
此时就可以启动这两段代码来检验是否能够实现“用户登录”这一功能,要注意的是必须首先启动服务器端,结果如下:
之后在运行客户端代码,服务器端显示结果如下:
可以看到服务器端成功接收了来自客户端的登录请求。
注意:
- 关于shutdownInput()方法及shutdownOutput()方法的理解如下:编写程序的大多数的时候是可以直接使用Socket类或输入输出流的close()方法关闭网络连接,但有时会出现希望只关闭输入输出流,并不关闭网络连接的情况。这就需要用到Socket类的shutdownInput()方法及shutdownOutput()方法,这两个方法只会关闭相应的输入、输出流,而并不会同时关闭网络连接,一般只有在确定不再需要网络连接的时候,才会使用Socket类或输入输出流的close()方法关闭相关资源。
- 关于打印流的相关内容可以到Java IO流查看;
关于端口号的相关内容可以到TCP/IP四层模型中第四部分“传输层”查看。
4、完善“用户登录功能”之实现服务器端的响应
之前仅仅实现了客户端向服务器端提交“用户信息”,但服务器端并没有向客户端发送反馈信息,进行响应,因此可在服务器端添加输出流,在客户端添加输入流,修改后的服务器端示例代码如下:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) {
try {
/*
* 创建服务器端Socket,即ServerSocket,并绑定监听端口
* 指定端口时,默认指定1023之后的端口号
* 此时会出现异常,使用try-catch块捕获该异常
*/
ServerSocket serverSocket = new ServerSocket(2333);
/*
* 调用accept()方法进行监听,等待客户端的连接
* 为了便于查看结果,添加一句输出
*/
System.out.println("****服务期即将启动,正在等待客户端连接****");
// 一旦调用该方法,服务器端就会进入阻塞状态,等待客户端连接
Socket socket = serverSocket.accept();
// 获取输入流,读取客户端信息
InputStream is = socket.getInputStream();
// 将字节输入流转换为字符输入流
InputStreamReader isr = new InputStreamReader(is);
// 为字符输入流添加缓冲
BufferedReader br = new BufferedReader(isr);
String info = null;
// 循环读取客户端信息
while ((info = br.readLine()) != null) {
System.out.println("服务器端——读取到客户端提交如下信息:" + info);
}
// 将输入流置于末尾
socket.shutdownInput();
// 获取输出流,响应客户端的请求
OutputStream os = socket.getOutputStream();
// 将字节输出流包装为打印流
PrintWriter pw = new PrintWriter(os);
// 编写服务器端的响应信息
pw.write("欢迎登录!");
// 通过刷新实现发送
pw.flush();
// 关闭相关资源
pw.close();
os.close();
br.close();
is.close();
socket.close();
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
修改后的客户端示例代码如下:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
public class Client {
public static void main(String[] args) {
try {
/*
* 创建客户端Socket,指定服务器地址和端口
* 本案例中,服务器端和客户端都在本地同一台主机中
* 因此服务器地址可填“127.0.0.1”或“localhost”
* 此时会出现异常,使用try-catch块捕获该异常
*/
Socket socket = new Socket("localhost", 2333);
// 获取输出流,向服务器端发送信息
OutputStream os = socket.getOutputStream();
// 将字节输出流包装为打印流
PrintWriter pw = new PrintWriter(os);
// 编写要向服务器端发送的信息
pw.write("用户名:admin;密码:123456");
// 通过刷新实现发送
pw.flush();
// 禁用输出流
socket.shutdownOutput();
// 获取输入流,读取服务器端的响应信息
InputStream is = socket.getInputStream();
// 将字节输入流转换为字符输入流
InputStreamReader isr = new InputStreamReader(is);
// 为字符输入流添加缓冲
BufferedReader br = new BufferedReader(isr);
String info = null;
// 循环读取客户端信息
while ((info = br.readLine()) != null) {
System.out.println("客户端——读取到服务器端反馈如下信息:" + info);
}
// 关闭相关资源
br.close();
is.close();
pw.close();
os.close();
socket.close();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
此时启动服务器端,结果如下:
之后在运行客户端代码,服务器端显示结果如下:
此时客户端结果如下:
可以看到客户端成功接收了来自服务器端的响应信息。
5、使用多线程实现服务器与多客户端进行通信
之前的案例仅仅实现了一台客户端与服务器进行通信的过程,但在实际生活中,往往是在服务器上运行一个永久的程序,而且可以与多个客户端进行通信,并且可以接收多个客户端的请求,提供相应的服务。
关于多线程的相关内容可以到已完结的深入浅出Java多线程专题查看。
使用多线程实现的具体步骤:
- 创建服务器端ServerSocket对象,循环调用accept()方法等待客户端的连接
- 客户端创建socket并请求与服务器端连接
- 服务器端接受客户端的请求,创建socket与该客户端建立专线连接
- 建立专线连接的服务器端socket与客户端socket在一个单独的线程上对话
- 之后服务器端继续等待新的客户端连接
首先创建单独的线程类,用来处理客户端的请求,示例代码如下:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
/*
* 此类为服务器线程处理类
* 继承Thread类
* 每当有客户端发送请求
* 服务器端都会创建一个Socket与之通信
*/
public class ServerThread extends Thread {
// 创建与本线程相关的Socket
Socket socket = null;
// 使用构造方法初始化Socket
public ServerThread(Socket socket) {
this.socket = socket;
}
// 线程执行操作,响应客户端的请求,重写父类的run()方法
public void run() {
// 初始化输入输出流
InputStream is = null;
BufferedReader br = null;
OutputStream os = null;
PrintWriter pw = null;
try {
is = socket.getInputStream();
// 将字节输入流转换为字符输入流
InputStreamReader isr = new InputStreamReader(is);
br = new BufferedReader(isr);
String info = null;
// 循环读取客户端信息
while ((info = br.readLine()) != null) {
System.out.println("服务器端——读取到客户端提交如下信息:" + info);
}
// 将输入流置于末尾
socket.shutdownInput();
os = socket.getOutputStream();
pw = new PrintWriter(os);
// 编写服务器端的响应信息
pw.write("欢迎登录!");
// 通过刷新实现发送
pw.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
/*
* 为了保证相关资源一定能够关闭 将其放置在finally代码块中
* 并增加if判断条件验证是否为空,避免报错
* 最后再统一用try-catch块包围处理异常
*/
try {
if (pw != null) {
pw.close();
}
if (os != null) {
os.close();
}
if (br != null) {
br.close();
}
if (is != null) {
is.close();
}
if (socket != null) {
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
之后创建服务器端,示例代码如下:
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) {
try {
/*
* 创建服务器端Socket,即ServerSocket,并绑定监听端口
* 指定端口时,默认指定1023之后的端口号
* 此时会出现异常,使用try-catch块捕获该异常
*/
ServerSocket serverSocket = new ServerSocket(2333);
// 为了便于查看结果,添加一句输出
System.out.println("****服务期即将启动,正在等待客户端连接****");
// 初始化Socket
Socket socket = null;
// 记录连接过的客户端数量
int count = 0;
// 循环监听等待客户端的连接
while (true) {
/*
* 调用accept()方法进行监听,一旦调用该方法
* 服务器端就会进入阻塞状态,等待客户端连接
*/
socket = serverSocket.accept();
// 创建一个新的线程
ServerThread serverThread = new ServerThread(socket);
/*
* 设置线程优先级,范围是[1,10],默认是5
* 未设置线程优先级可能会导致运行时速度非常慢
* 可降低优先级
*/
serverThread.setPriority(4);
// 启动线程
serverThread.start();
// 统计连接过的客户端的数量
count++;
System.out.println("已累计服务" + count + "台客户端!");
// 获取连接的客户端的IP地址
InetAddress address = socket.getInetAddress();
System.out.println("当前客户端的IP地址是:" + address.getHostAddress());
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
最后创建客户端,示例代码如下:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
public class Client {
public static void main(String[] args) {
try {
/*
* 创建客户端Socket,指定服务器地址和端口
* 本案例中,服务器端和客户端都在本地同一台主机中
* 因此服务器地址可填“127.0.0.1”或“localhost”
* 此时会出现异常,使用try-catch块捕获该异常
*/
Socket socket = new Socket("localhost", 2333);
// 获取输出流,向服务器端发送信息
OutputStream os = socket.getOutputStream();
// 将字节输出流包装为打印流
PrintWriter pw = new PrintWriter(os);
// 编写要向服务器端发送的信息
pw.write("用户名:admin;密码:123456");
// 通过刷新实现发送
pw.flush();
// 禁用输出流
socket.shutdownOutput();
// 获取输入流,读取服务器端的响应信息
InputStream is = socket.getInputStream();
// 将字节输入流转换为字符输入流
InputStreamReader isr = new InputStreamReader(is);
// 为字符输入流添加缓冲
BufferedReader br = new BufferedReader(isr);
String info = null;
// 循环读取客户端信息
while ((info = br.readLine()) != null) {
System.out.println("客户端——读取到服务器端反馈如下信息:" + info);
}
// 关闭相关资源
br.close();
is.close();
pw.close();
os.close();
socket.close();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
此时启动服务器端,结果如下:
之后在运行客户端代码,服务器端显示结果如下:
之后修改客户端的用户名为“Tom”,密码为“654321”,模拟多客户端登录,重新运行客户端代码,服务器端结果如下:
可见成功实现了服务器端与多个客户端进行通信。
版权声明:欢迎转载,欢迎扩散,但转载时请标明作者以及原文出处,谢谢合作! ↓↓↓
网友评论