TCP的API
ServerSocket
Socket
TCP回显服务器
TCP的连接管理是由操作系统内核管理
使用阻塞队列组织若干个连接对象
当连接建立成功,内核已经把这个连接对象放入到阻塞队列当中,代码中调用的accept就是从阻塞队列中取出一个连接对象(在应用程序中的化)
后续的数据读写都是针对clientSocket这个对象来进行展开的
如果服务器启动之后,没有客户端建立连接,此时代码中调用accept就会阻塞,阻塞到真的有客户端建立连接。
服务器的处理方式:
1.短连接: 一个连接中,客户端和服务器之间只交互一次.交互完毕,就断开连接.
2.长连接: 一个连接中,客户端和服务器之间交互N次.直到满足一定条件再断开连接.(效率更高,避免了反复建立连接和断开连接的过程)
1、初始化服务器
2、进入主循环
a、从内核获得TCP连接
b、处理TCP连接
(1)读取请求并解析
(2)根据请求计算响应
(3)把响应写回给客户端
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class TcpEchoServer {
//1、初始化服务器
//2、进入主循环
//a、从内核获得TCP连接
//b、处理TCP连接
//(1)读取请求并解析
//(2)根据请求计算响应
//(3)把响应写回给客户端
private ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
//类似UCP服务器绑定端口
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
while (true) {
//1)先从内核中获取到一个TCP连接
Socket clientSocket = serverSocket.accept();
//2)处理连接
processConnection(clientSocket);
}
}
private void processConnection(Socket clientSocket) {
System.out.printf("[%s : %d] 客户端上线\n", clientSocket.getInetAddress().toString(),
clientSocket.getPort());
//通过clientSocket 来和客户端交互,先做好准备工作,获取到clientSocket中的流对象
try(BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()))) {
//实现长连接 去掉while循环就是短链接
//一次连接的处理过程中
//当客户端断开连接退出循环,也就结束
//当客户端断开连接的时候readLine 或者 write方法会触发异常
while (true) {
//1、读取请求并解析(readLine()对应客户端发送数据的格式,必须是按行发送)
String request = bufferedReader.readLine();//客户端发的数据必须是一个按行发送的数据
// (相当于应用层的自定义协议,客户端发送也必须按行发送)
//2、根据请求计算响应
String response = process(request);
//3、把响应写回到客户端(客户端按行读取)
bufferedWriter.write(response+"\n");
System.out.printf("[%s : %d] ip :%s ; 端口: %s \n;",clientSocket.getInetAddress().toString(),
clientSocket.getPort(),request,response);
}
} catch (IOException e) {
//e.printStackTrace();
System.out.printf("[%s : %d] 客户端下线\n",clientSocket.getInetAddress().toString(),
clientSocket.getPort());
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer tcpEchoServer = new TcpEchoServer(9090);
tcpEchoServer.start();
}
}
TCP服务器优化
TCP要保证可靠传输,所以牺牲了一定的性能。
优化一(缓冲区刷新)
上述代码中,客户端连接之后,服务器能感受到客户端,但是客户端发送数据,服务器没有反馈。
客户端发送数据给服务器之后,服务器就要做出相应的.
服务器没有任何提示,说明此时大概率的情况是:
1.客户端没有发送请求成功
2.服务器没有接受请求成功
没有打印日志,在readLine()堵塞
查看服务器线程
然后打开客户端的线程、
使用BuffferedWriter的write将数据写入了缓冲区,并没有真的写入到socket文件中,所以要手动刷新缓冲区 使用flush方法
服务器加flush
客户端加flush
测试
启动多个连接
在 idea中编译运行的客户端可以正常运行,但是在命令行中运行的客户端不能正常回响
只有当退出第一个在idea中的客户端,这样在shell中的客户端才正常上线。
说明现在的服务器同一时刻只能处理一个客户端的连接. (同时过来多个客户端,此时只有第一个能正确处理. 第一个客户端退出了, 第二个客户端
才能正确被处理)
原因在于accept方法调用速度太慢、频率太低,调用几次accept就处理几个客户端的连接
解决方案:
需要在代码中,同时调用accept和processConnection (让这两个操作,并发执行)
多线程:用一个线程专门负责调用accept,再用其他线程,每个线程专负责一个客户端的长连接循环
原来的代码, accept和processConnection是串行执行的,改进后accept和processConnection是并发执行的。此处代码中while就会反复快速的调用到accept.于是就能同时处理多个客户端的
连接了
注意
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class TcpThreadEchoServer {
private ServerSocket serverSocket = null;
public TcpThreadEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
while (true){
Socket clientSocket = serverSocket.accept();
//针对这个连接,单独创建一个线程负责处理
Thread t = new Thread(){
@Override
public void run() {
try {
processConnection(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
}
};
t.start();
}
}
private void processConnection(Socket clientSocket) throws IOException {
System.out.printf("[%s : %d] 客户端上线\n",clientSocket.getInetAddress().toString(),
clientSocket.getPort());
try(BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()))){
while (true){
//1、读取请求并解析
String request = bufferedReader.readLine();
//2、根据请求计算响应
String response = process(request);
//3、把相应写回到客户端
bufferedWriter.write(response + "\n");
bufferedWriter.flush();
System.out.printf("[%s : %d] ip:%s; 端口:%s\n",clientSocket.getInetAddress().toString(),
clientSocket.getPort(),request,response);
}
}catch (IOException e){
// e.printStackTrace();
System.out.printf("[%s : %d] 客户端下线\n",clientSocket.getInetAddress().toString(),
clientSocket.getPort());
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpThreadEchoServer tcpThreadEchoServer = new TcpThreadEchoServer(9090);
tcpThreadEchoServer.start();
}
}
线程池优化
避免频繁的创建和销毁线程,节省开销
最终的服务器优化代码
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TcpThreadPoolEchoServer {
private ServerSocket serverSocket = null;
public TcpThreadPoolEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
//创建一个线程池
ExecutorService executorService = Executors.newCachedThreadPool();
while (true){
Socket clientSocket = serverSocket.accept();
//针对这个连接,单独创建一个线程负责处理
executorService.execute(new Runnable() {
@Override
public void run() {
processConnection(clientSocket);
}
});
}
}
private void processConnection(Socket clientSocket) {
System.out.printf("[%s : %d] 客户端上线\n",clientSocket.getInetAddress().toString(),
clientSocket.getPort());
try(BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()))){
while (true){
//1、读取请求并解析
String request = bufferedReader.readLine();
//2、根据请求计算响应
String response = process(request);
//3、把相应写回到客户端
bufferedWriter.write(response + "\n");
bufferedWriter.flush();
System.out.printf("[%s : %d] ip:%s; 端口:%s\n",clientSocket.getInetAddress().toString(),
clientSocket.getPort(),request,response);
}
}catch (IOException e){
// e.printStackTrace();
System.out.printf("[%s : %d] 客户端下线\n",clientSocket.getInetAddress().toString(),
clientSocket.getPort());
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpThreadPoolEchoServer tcpThreadPoolEchoServer = new TcpThreadPoolEchoServer(9090);
tcpThreadPoolEchoServer.start();
}
}
TCP回显客户端
1、启动客户端(不绑定端口)和服务器建立连接
2、进入主循环
a、读取用户输入内容
b、构造一个请求发送给服务器
c、读取服务器的响应数据
d、把响应数据显示到界面
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoClient {
//1、启动客户端(不绑定端口)和服务器建立连接
//2、进入主循环
// a、读取用户输入内容
// b、构造一个请求发送给服务器
// c、读取服务器的响应数据
// d、把响应数据显示到界面
private Socket socket = null;
public TcpEchoClient(String serverIp,int serverPort) throws IOException {
//从此处的实例化Socket过程,就是建立Ip连接
socket = new Socket(serverIp,serverPort);
}
public void start() throws IOException {
System.out.println("客户端启动");
Scanner in = new Scanner(System.in);
try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()))){
while (true){
// a、读取用户输入内容
System.out.println("->");
String request = in.nextLine();
if("exit".equals(request)){
break;
}
// b、构造一个请求发送给服务器
bufferedWriter.write(request + "\n");
//\n是为了和服务器中的readLine相对应 读取和发送都是按行发送读取
//按行读 按行写是一种简单的自定义协议
// c、读取服务器的响应数据
String response = bufferedReader.readLine();//读取响应和服务器返回响应对应,只读一行忽略\n
// 自定义协议请求和响应不一定要一致
// d、把响应数据显示到界面
System.out.println(response);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient tcpEchoClient = new TcpEchoClient("127.0.0.1",9090);
//127.0.0.1表示环回ip,自己访问自己,刚刚所写的服务器和客户端都是在一个主机上,所以使用这个ip
//如果不在同一个主机上,此处的ip就要写成要访问的服务器ip,9090就是上面写的服务器的端口号
tcpEchoClient.start();
}
}