文章目录
- 一:Java流套接字通信模型
- 二:相关API详解
- (1)ServerSocket
- (2)Socket
- 三:TCP通信示例一:客户端发送什么服务端就返回什么
- (1)代码
- (2)效果展示
- (3)分析
- 四:TCP通信示例二:多线程版本
- (1)单线程版本存在的问题
- (2)代码
- (3)效果展示
- 五:TCP通信示例三:线程池版本
- (1)多线程版本存在的问题
- (2)代码
- (3)效果展示
一:Java流套接字通信模型
Java TCP通信模型:Java中使用TCP协议进行通信,主要依靠以下两个类
ServerSocket
:是创建TCP服务端Socket的APISocket API
:是客户端Socket,或服务端中接收到客户端连接(accept方法)的请求后,返回服务端Socket
通信流程如下
二:相关API详解
(1)ServerSocket
ServerSocket:用于创建TCP服务端流套接字Socket
构造方法如下
方法签名 | 方法说明 |
---|---|
ServerSocet(int port) | 创建一个服务端流套接字 Socket ,并绑定到指定端口 |
成员方法如下
方法签名 | 方法说明 |
---|---|
Socket accept() | 开始监听端口,当有客户端连接后会返回一个服务端Socket 对象,并基于该Socket 与客户端建立连接,否则阻塞等待 |
void close() | 关闭此套接字 |
(2)Socket
Socket :是客户端的Socket
(当然也会给服务端用,上面表格说过,当有客户端连接服务端后,会返回一个服务端Socket
)
构造方法如下
方法签名 | 方法说明 |
---|---|
Socket(String host, int port) | 创建一个客户端流套接字Socket ,并和对应IP 的主机上、对应端口的进程建立连接 |
成员方法如下
方法签名 | 方法说明 |
---|---|
InetAddress getInetAddress() | 返回套接字所连接的地址 |
InputStream getInputStream() | 返回套接字的输入流 |
OutputStream getOutputStream() | 返回此套接字的输出流 |
三:TCP通信示例一:客户端发送什么服务端就返回什么
- 注意:这个功能比较简单,但主要目的是为了演示上面所讲API的用法
(1)代码
服务端IP
地址设置为127.0.0.1
,也即本地环回,也即自己发自己收,数据会完整走一遍协议
服务端:TCPServer:
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class TCPServer {
// 创建监听套接字
private ServerSocket listenSocket = null;
public TCPServer(int port) throws IOException {
// 监听套接字绑定指定端口
listenSocket = new ServerSocket(port);
}
// 服务器启动
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
// 调用监听套接字的accept()连接客户端,并返回Socket类型的clientSocket
// 将clientSocket传递给具体处理连接的方法processConnection()进行处理
Socket clientSocket = listenSocket.accept();
// 进行处理
processConnection(clientSocket);
}
}
// 用于处理连接
private void processConnection(Socket clientSocket) throws IOException {
System.out.println("【客户端IP: " + clientSocket.getInetAddress().toString()
+ "客户端口号:" + clientSocket.getPort() + "】"
+ "已上线");
// 处理请求
// 打开inputStream和outputStream
try (InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
while (true) {
// 1. 读取请求并解析
Scanner scanner = new Scanner(inputStream);
if (!scanner.hasNext()) {
// 如果读完了那么连接可以断开了
System.out.println("【客户端IP: " + clientSocket.getInetAddress().toString()
+ "客户端口号:" + clientSocket.getPort() + "】"
+ "下线");
break;
}
String request = scanner.next();
// 2. 根据请求计算响应,具体处理函数为process
String response = process(request);
// 3. 响应回复给客户端
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(response);
printWriter.flush();
// 打印信息
System.out.println("【客户端IP: " + clientSocket.getInetAddress().toString()
+ "客户端口号:" + clientSocket.getPort() + "】"
+ ":\"" + request + "\"" + ", 服务端回复: " + "\"" + response + "\"");
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭套接字
// listenSocket在TCP服务端程序中只有一个,所以不太可能把文件描述符占满
// 而clientSocket 每遇到一个客户端都要创建一个,所以一定要注意关闭
clientSocket.close();
}
}
// 业务逻辑函数
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TCPServer server = new TCPServer(9090);
server.start();
}
}
客户端:TPCClient:
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 TCPClient {
// 建立Socket对象
private Socket socket = null;
public TCPClient(String serverIP, int serverPort) throws IOException {
// 指定服务端IP和端口号
socket = new Socket(serverIP, serverPort);
}
// 客户端启动
public void start () throws IOException {
Scanner scanner = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
while (true) {
// 1. 获取用户输入
System.out.print("input: ");
String request = scanner.next();
// 2. 发送请求给服务端
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(request);
printWriter.flush();
// 3. 从服务端获得响应
Scanner responseScanner = new Scanner(inputStream);
String response = responseScanner.next();
// 打印信息
System.out.println("服务端回复:" + response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TCPClient client = new TCPClient("127.0.0.1", 9090);
client.start();
}
}
(2)效果展示
(3)分析
对于服务端(TCPServer
类):
-
构造方法(
public TCPServer(int port)
)- 需要建立一个
ServerSocket
类型的监听套接字,用于监听客户端的请求连接,也即private ServerSocket listenSocket = new ServerSocket(port)
- 需要建立一个
-
服务端处理逻辑(
public void start()
)- 不断循环一直监听客户端的连接,当有客户端连接之后监听套接字会返回
Socket
类型的套接字用于处理这个连接,也即Socket clientSocket = listenSocket.accept()
- 具体处理连接的过程交由
processConnection()
方法进行,也即processConnection(ClientSocket)
- 不断循环一直监听客户端的连接,当有客户端连接之后监听套接字会返回
-
服务端处理连接(
private void processConnection(Socket clientSocket)
)-
①:打开套接字的输入流和输出流
clientSocket
里的请求内容保存在其InputStream
中,最终服务端回复响应时要将该响应写入到其OutputStream
中,也即InputStream inputStream = clientSocket.getInputStream()
和OutputStream outputStream = clientSocket.getOutputStream()
-
②:读取
InputStream
中的请求并解析- 使用
Scanner
进行读取比较方便,也即Scanner scanner = new Scanner(inputStream)
- 读取时注意随时判断是否读取完毕,如果读取完毕表示客户端可以下线了
- 读取好的请求保存在
request
中,也即String request = scanner.next()
- 使用
-
③:根据请求得到响应
- 拿
request
后,需要对该request
进行处理(交给方法process
),不同的业务逻辑会有不同的处理方法。这里我们只是简单的“回显”一下即可,也即客户端发什么服务端就回复什么
- 拿
-
④:将响应写入到
OutputStream
中- 使用
PrintWriter
写入比较方便,也即PrintWriter printWriter = new PrintWriter(outputStream)
、printWriter.println(response)
- 写入完成之后必要忘记刷新一下,也即
printWriter.flush()
- 使用
-
⑤:打印相关信息
-
⑥:关闭
clientSocket
套接字listenSocket
在TCP服务端程序中只有一个,所以不太可能把文件描述符占满,而clientSocket
每遇到一个客户端都要创建一个,所以一定要注意关闭,也即clientSocket.close()
-
-
main
方法- 构造
TCPServer
对象,并绑定指定端口号,如9090
,也即TCPServer server = new TCPServer(9090)
- 启动服务端,也即
server.start()
- 构造
对于服务端(TCPClient
类):
-
构造方法(
public TCPClient(String serverIP, int serverPort)
)- 需要建立一个
Socket
类型的套接字,并传入服务端IP
和Port
,也即private Socket socket = new Socket(serverIP, serverPort)
- 需要建立一个
-
客户端处理逻辑(
public void start ()
)-
①:打开套接字的输入流和输出流
- 客户端会把它的请求写入到
InputStream
中,服务端回复响应后客户端会从OutputStream
中读取,也即InputStream inputStream = socket.getInputStream()
、OutputStream outputStream = socket.getOutputStream()
- 客户端会把它的请求写入到
-
②:读取客户端用户输入并构造请求
- 使用
Scanner
接受即可,也即String request = scanner.next()
- 使用
-
③:将请求写入到
OutputStream
中- 使用
PrintWriter
写入比较方便,也即PrintWriter printWriter = new PrintWriter(outputStream)
、printWriter.println(request)
- 写入完成之后必要忘记刷新一下,也即
printWriter.flush()
- 使用
-
④:读取
InputStream
中的响应- 使用
Scanner
进行读取比较方便,也即Scanner responseScanner = new Scanner(inputStream)
、String response = responseScanner.next()
- 使用
-
⑤:打印相关信息
-
-
main
方法- 构造
TCPClient
对象,并给定服务端IP
和Port
,也即TCPClient client = new TCPClient("127.0.0.1", 9090)
- 启动客户端,也即
client.start()
- 构造
四:TCP通信示例二:多线程版本
(1)单线程版本存在的问题
上面的例子中,如果让多个客户端连接服务端会存在如下问题,以两个客户端为例
- 客户端1连接服务端后,服务端提示“客户端1上线”
- 客户端2连接服务端后,服务端未提示“客户端2上线”
- 客户端1发送“客户端1”后服务端接受并正确返回
- 客户端2发送“客户端2”后服务端似乎没有接受到消息,也没有什么反应
- 客户端1结束运行,服务端提示“客户端1下线”,此时刚才客户端2发送的“客户端2”立刻显示同时服务端也正确回复
产生这样的现象原因在于服务端整个处理逻辑中只有一个线程,所以服务端在处理客户端1的请求时会被阻塞在下面代码中
Socket clientSocket = listenSocket.accept();
processConnection(clientSocket);
if (!scanner.hasNext()) {
System.out.println("【客户端IP: " + clientSocket.getInetAddress().toString()
+ "客户端口号:" + clientSocket.getPort() + "】"
+ "下线");
break;
}
当客户端1下线之后,服务端立马收到客户端2的请求然后才会去处理。所以要解决这个问题,整个代码逻辑必须使用多线程的方式进行改写
(2)代码
改写也比较简单,主线程持续监听客户端连接,每当一个客户端连接时便创建一个线程执行processConnection
方法
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
// 调用监听套接字的accept()连接客户端,并返回Socket类型的clientSocket
// 将clientSocket传递给具体处理连接的方法processConnection()进行处理
// 主线程一直负责监听客户端连接
Socket clientSocket = listenSocket.accept();
// 每来一个客户端使用一个线程处理
Thread thread = new Thread(){
@Override
public void run(){
try {
processConnection(clientSocket);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
};
thread.start();
}
}
(3)效果展示
如下,创建两个客户端
五:TCP通信示例三:线程池版本
(1)多线程版本存在的问题
采用多线程最大的问题在于当客户端数量一多就会涉及到频繁的线程创建和销毁,这开销会很大,所以为了减小创建销毁开销,同时也为了增加程序稳定性,这里我们可以使用线程池完成
- Java线程池用法
(2)代码
改写也比较简单,主线程持续监听客户端连接,每当一个客户端连接时便使用线程池中的线程去执行processConnection
方法
public void start() throws IOException {
System.out.println("服务器启动!");
// 创建一个线程池
ExecutorService service = Executors.newCachedThreadPool();
while (true) {
// 调用监听套接字的accept()连接客户端,并返回Socket类型的clientSocket
// 将clientSocket传递给具体处理连接的方法processConnection()进行处理
// 主线程一直负责监听客户端连接
Socket clientSocket = listenSocket.accept();
// 每来一个客户端使用线程池中的线程处理
service.submit(new Runnable() {
@Override
public void run() {
try {
processConnection(clientSocket);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
}
}