上篇文章:
目录
[1 TCP流套接字](#1 TCP流套接字)
[2 模拟实现TCP服务器](#2 模拟实现TCP服务器)
1 TCP流套接字
基于TCP的Socket主要有:ServerSocket和Socket,ServerSocket用于创建TCP服务器端的Socket,而Socket用于创建TCP客户端的Socket。操作方式也类似文件。
|------------------------|----------------------------------------------------------------------------|
| 构造方法/方法 | 含义 |
| ServerSocket(int port) | 构造方法,创建一个服务端流套接字Socket,并绑定到指定端口 |
| Socket accept() | 普通方法,开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待 |
| void close() | 关闭TCP套接字 |
因为TCP是面向流的数据读写方式,因此没有像DatagramPacket数据报的API,只需创建Socket后,采用类似InputStream和OutputStream的操作方式。也可以对InputStream和OutputStream进行Scanner和PrintWriter的包装,便于字符数据的读写。
|--------------------------------|-----------------------------------------------|
| 构造方法/方法 | 含义 |
| Socket(String host, int port) | 构造方法,创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接 |
| InetAddress getInetAddress() | 从套接字中获取连接的IP地址 |
| InputStream getInputStream() | 返回套接字中的输入流(读请求) |
| OutputStream getOutputStream() | 返回套接字中的输出流(写响应) |
注意:Socket可能有两种获得的方式,1是使用Socket构造方法,2是使用ServerSocket的方法accept()。也就是说ServerSocket的主要作用就是创建TCP服务器的全局连接监听,客户端作为连接发起方,因此直接创建Socket表示申请建立连接,而ServerSocket的accept()方法一旦监听到有客户端申请建立连接,就返回一个Socket用于建立服务器和客户端之间的连接。
上述分析方式也透露了ServerSocket和Socket的生命周期,ServerSocket的生命周期伴随整个服务器进程,而Socket的生命周期只是一次连接周期。
2 模拟实现TCP服务器
java
public class TcpServer {
//服务器端口号
private final int PORT = 8000;
//创建服务器
private ServerSocket serverSocket = null;
public TcpServer() throws IOException {
serverSocket = new ServerSocket(PORT);
}
//启动服务器
public void start() throws IOException {
System.out.println("服务器启动成功");
ExecutorService executorService = Executors.newCachedThreadPool();
while(true){
//将建立的TCP连接拿到应用程序中(accept()会阻塞,直到建立连接)
Socket clientSocket = serverSocket.accept();
//[版本1]直接调用processConnect()就会导致第一个客户端连接执行到该方法while中,服务器线程从而无法执行accept
//进而无法一个服务器为多个客户端服务
//[版本2]解决方案:多线程(一个线程accept(),一个线程processConnect())(新的问题:频繁创建销毁线程)
// Thread t = new Thread(() ->{
// try {
// processConnect(clientSocket);
// } catch (IOException e) {
// e.printStackTrace();
// }
// });
// t.start();
//[版本3]解决方案:线程池(新的问题:线程数量太多了(IO多路复用->NIO))
executorService.submit(new Runnable() {
@Override
public void run() {
try {
processConnect(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
//给当前连接的客户端提供服务(一个连接只进行一次数据交互服务(短连接)||一个连接进行多次数据交互服务(长连接))
//长连接版本(去掉循环就是短连接版本)
public void processConnect(Socket clientSocket) throws IOException {
System.out.printf("[%s:%d] 建立连接\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()){
Scanner scanner = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream);
while(true){
if(!scanner.hasNext()){
//如果没有请求说明客户端断开连接
System.out.printf("[%s:%d] 断开连接\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
break;
}
//1.读取请求并解析
String request = scanner.next();
//2.根据请求计算响应
String response = process(request);
//3.响应写回客户端
// (注意此处不能使用next()类的函数,因为这类函数读取结束标志是空白符:换行符、回车符等,输入没有这些符号服务器就会被阻塞在这类函数)
printWriter.println(response);
//刷新一下缓冲区
printWriter.flush();
System.out.printf("[%s:%d] request:%s, response:%s\n",clientSocket.getInetAddress().toString(),clientSocket.getPort()
,request,response);
}
}finally {
//连接用完需要关闭(clientSocket生命周期是一次连接周期,而serverSocket生命周期是整个服务器运行周期)
clientSocket.close();
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpServer tcpServer = new TcpServer();
tcpServer.start();
}
}
public class TcpClient {
//创建客户端
private Socket socket = null;
public TcpClient() throws IOException {
//new对象时就是和TCP服务器建立连接(因此需要直到服务器地址)
socket = new Socket("127.0.0.1",8000);
}
//启动服务器
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()){
Scanner scannerNet = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream);
while(true){
//1.读取用户输入
System.out.print(">");
//注意此时next()读取到换行就结束了,但是读取的数据不含空白符,即没有回车符
String request = scanner.next();
//2.发送请求
// (注意此处不能使用next()类的函数,因为这类函数读取结束标志是空白符:换行符、回车符等,输入没有这些符号服务器就会被阻塞在这类函数)
printWriter.println(request);
printWriter.flush();
//3.接收响应
String response = scannerNet.next();
//4.将响应返回给用户
System.out.printf("request:%s, response:%s\n",request,response);
}
}
}
public static void main(String[] args) throws IOException {
TcpClient tcpClient= new TcpClient();
tcpClient.start();
}
}
运行结果如下:



上述代码需要注意3点:
**1.服务器端什么时候该关闭clientSocket(即关闭连接)?**当服务器端processConnect方法内部从循环跳出时,证明此时客户端没有数据要发送,此时可以关闭连接,采用try-catch-finally方式,防止出现异常无法正常关闭。
**2.如何处理next()引起的阻塞问题?**上述代码很多地方可能要用到Scanner的next()方法,但是该方法会读取到空白符(回车换行等)才能结束,当客户端输入数据时可能不会携带空白符(在命令行中敲回车,该回车会被接收数据的next识别,发送的请求中并不携带回车符),此时就会导致服务器端一直未识别到结束,从而一直无响应。解决的办法就是在发送的数据中添加空白符,比如使用println()方法会自动在数据结尾添加回车符。
**3.如何解决服务器端只能为一个客户端服务?**当不采用多线程方案时,第一个客户端建立连接发送请求,进入processConnect方法内部时,服务器端的主线程就会进入while中,从而其他客户端申请建立连接时,服务器主线程无法通过accept()监听建立连接的申请。采用多线程方案,线程池实现一个线程为一个客户端服务(注意,当并发量很大时,线程池的线程数量很多,就会导致资源浪费调度困难等问题,此时需要采用NIO(非阻塞IO)的方式,这是一种I/O多路复用的技术,可以实现一个线程管理多个客户端)。
下篇文章: