这篇文章,带你从最简单的tcp服务器入手。再到NIO 服务器,学会用netty高性能框架搭建各种基于Tcp协议的服务器。
该过程全部针对的是java Tcp服务器,所以,TCP客户端使用工具即可(我这里使用的是友善串口助手)。
1.java最简单的Tcp服务:
代码:
public class TcpDemo01 {
public static void main(String[] args) throws IOException {
// 1.创建Tcp服务器并绑定端口
ServerSocket serverSocket = new ServerSocket(8111);
// 2.等待连接
Socket socket = serverSocket.accept();
// 3.打印连接内容
System.out.println(socket);
}
}
解释:
java创建tcp服务器是极其简单的。
- new ServerSocket(8111)就能创建一个Tcp服务器。
- 执行到serverSocket.accept()方法。代码会卡(阻塞)在这个方法,不执行后面的代码。当客户端连接进来时,该方法就不卡(阻塞)。同时会返回客户端连接信息。
- 打印的信息,addr客户端的ip地址,port客户端的端口,localport前面设置的服务器端口。

2.保持连接,接收客户端信息
代码:
public class TcpDemo02 {
public static void main(String[] args) throws IOException {
// 1.创建Tcp服务器并绑定端口
ServerSocket serverSocket = new ServerSocket(8111);
// 2.等待连接
Socket socket = serverSocket.accept();
// 3.打印连接内容
System.out.println(socket);
// 4.拿到输入流
InputStream inputStream = socket.getInputStream();
// 5.等待客户端发信息过来
int readData = inputStream.read();
System.out.println((char) readData);
}
}
-
当TcpServer和TcpClient连接完成之后,他们之间就会有通道。我们拿到输入流是针对Server而言。即输入到Server的流。
-
inputStream.read();方法也会阻塞,当客户端发送数据过来的时候才会取消阻塞。
-

-
客户端发送的是ascii的123,所以服务器要将ascii转成字符(char) readData。通过测试我们发现,客户端发送的是123,但服务只打印了1。read()方法读取流中的数据是挨个读的。读到一个数据1后,代码继续执行打印,后面没有代码了,服务器就断开连接了流也断开了。所以只读到一个。
-
解决读取客户端数据不完整问题:
-
read()有带参数的方法重载read(byte b[])。设置byte b[]数组的大小就能设置每次读取多少个数据。代码:
public class TcpDemo02 { public static void main(String[] args) throws IOException { // 1.创建Tcp服务器并绑定端口 ServerSocket serverSocket = new ServerSocket(8111); // 2.等待连接 Socket socket = serverSocket.accept(); // 3.打印连接内容 System.out.println(socket); // 4.拿到输入流 InputStream inputStream = socket.getInputStream(); byte bs[] = new byte[3]; // 5.等待客户端发信息过来 int readData = inputStream.read(bs); System.out.println(new String(bs)); } } 但显然这种方式不合理,虽然接收到了客户端完整123信息,但只能接收到一次,服务器还是会断开。而且我们是不知道客户端要传多少信息过来的。当,接收大小 < 发送大小。数据还是会发生丢失。
-
循环读取,代码:
public class TcpDemo02 { public static void main(String[] args) throws IOException { // 1.创建Tcp服务器并绑定端口 ServerSocket serverSocket = new ServerSocket(8111); // 2.等待连接 Socket socket = serverSocket.accept(); // 3.打印连接内容 System.out.println(socket); // 4.拿到输入流 InputStream inputStream = socket.getInputStream(); while (true) { // 5.等待客户端发信息过来 int readData = inputStream.read(); System.out.println((char) readData); // 客户端发送完成时read()方法会返回-1 if (readData == -1) { System.out.println("接收完成断开连接"); break; } } } } 通过一个循环,一直等待read()方法返回数据,这样子就能一直接收到客户端传来的信息了。有数据就执行打印,并且判断read()返回的数据是否是-1,-1表示客户端发送完成。循环读取,每次只读一个字符,如果客户端发送的数据量大,那么性能就会下降。结合上面两种接收方式做以下改进。
-
结合优化
public class TcpDemo02 { public static void main(String[] args) throws IOException { // 1.创建Tcp服务器并绑定端口 ServerSocket serverSocket = new ServerSocket(8111); // 2.等待连接 Socket socket = serverSocket.accept(); // 3.打印连接内容 System.out.println(socket); // 4.拿到输入流 InputStream inputStream = socket.getInputStream(); byte bs[] = new byte[1024]; while (true) { // 5.等待客户端发信息过来 int readData = inputStream.read(bs); System.out.println(new String(bs)); // 客户端断开连接时read()方法会返回-1 if (readData == -1) { System.out.println("客户端断开连接"); break; } } } }
-
3.服务器支持多个客户端
至此,就得到了一个能接收到单个客户端信息的Tcp服务器。这时候再打开一个tcp客户端工具,就会发现只能第一个连接的客户端发送消息能在服务器打印。这是为什么呢?
//根据上面的代码,观察这3行代码
Socket socket = serverSocket.accept();
InputStream inputStream = socket.getInputStream();
int readData = inputStream.read(bs);
不难发现,我们只维护了第一个socket,第一个输入流。后面连接进来的也没有覆盖第一个也没有进行管理。我们的服务器只能接收一个请求,这显然不合理。进一步优化!
首先我们要明确,如何才能使得两个客户端发送过来的消息都能进行处理。我们发现如果只在当前线程的话,只能接收处理一个连接。这时候引入多线程去做处理就能解决这些问题。(当然还有其他方法可以做到,后面讨论)
代码:
public class TcpDemo03 {
public static void main(String[] args) throws IOException {
// 1.创建Tcp服务器并绑定端口
ServerSocket serverSocket = new ServerSocket(8111);
while (true) {
// 2.等待连接
Socket socket = serverSocket.accept();
// 3.打印连接内容
System.out.println(socket + "连接成功");
// 4.每次有连接,创建一个线程这个连接管理
new Thread(() -> {
try {
InputStream inputStream = socket.getInputStream();
byte bs[] = new byte[1024];
while (true) {
// 5.等待客户端发信息过来
int readData = inputStream.read(bs);
// 客户端断开连接时read()方法会返回-1
if (readData == -1) {
System.out.println(socket + "断开连接");
break;
}
System.out.println(socket + "收到信息:" + new String(bs));
}
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}

解释:
每次有连接进来的时候,创建一个线程(lambda表达式),每个连接都单独处理,互不影响。这样子我们就能支持多个客户端的连接了。每次连接都会创建一个线程,客户端数量上来了就会卡顿,服务器扛不住这么多连接。既然如此再优化!
设置最大连接数量,并且我们不主动创建线程,由线程池去管理线程。
public class TcpDemo04 {
private static final int MAX_CONNECT = 200;// 设置最大连接数
private static ExecutorService threadPool = Executors.newFixedThreadPool(MAX_CONNECT);
public volatile static int connectedCount = 0;
public static void main(String[] args) throws IOException {
// 1.创建Tcp服务器并绑定端口
ServerSocket serverSocket = new ServerSocket(8111);
while (true) {
// 2.等待连接
Socket socket = serverSocket.accept();
connectedCount++;
if (connectedCount > MAX_CONNECT) {
socket.close();
connectedCount--;//连接关闭,连接数量-1
System.out.println("连接数量超过设定数据,阻止连接");
} else {
// 3.打印连接内容
System.out.println(socket + "连接成功");
threadPool.execute(new HandleThread(socket));
}
}
}
}
class HandleThread implements Runnable {
private Socket socket;
public HandleThread(Socket _socket) {
this.socket = _socket;
}
@Override
public void run() {
try {
InputStream inputStream = socket.getInputStream();
while (true) {
byte bs[] = new byte[1024];
// 5.等待客户端发信息过来
int readData = inputStream.read(bs);
// 客户端断开连接时read()方法会返回-1
if (readData == -1) {
System.out.println(socket + "断开连接");
TcpDemo04.connectedCount--;//连接关闭,连接数量-1
break;
}
System.out.println(socket + "信息:" + new String(bs));
}
} catch (Exception e) {
}
}
}
连接过来时,线程池管理这些连接。经过测试我们发现,当超出最大连接数量时,后面的连接就会断开。这样子就能防止过多连接影响性能。显然这种方式对超出连接数量后的连接不够友好。它们无论如何都不能连接到服务器,我们提供的服务器用户多了就会卡。明显我们要解决这个问题。
解决问题的方式就有:
-
大力飞砖:又称纵向扩展,即升级配置,增强服务器的硬件配置(如增加CPU、内存、存储等)来提升其性能。
-
负载均衡:又称横向扩展,即增加服务器数量,一个服务器不够用那就搞多几个服务器。
-
优化:充分利用电脑的性能,减少连接开销,减少处理开销。创建线程过多,线程上下文切换,以及每个线程都会占用一定的栈空间。serverSocket.accept(),socket.getInputStream().read()都是阻塞的,阻塞过程中CPU都不工作.
前面两种都是要钱的。那就只能尽量通过优化的方式去解决问题。