简单的服务器 TCP版本 熟悉其 API 网络
Posted 一朵花花
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了简单的服务器 TCP版本 熟悉其 API 网络相关的知识,希望对你有一定的参考价值。
前言: 上篇,我们写了 UDP 版本的简单服务器,本篇通过写一个简单的服务器来了解 TCP 的常用API
TCP 版本 (以长连接为例)
面向字节流
服务器程序
流程梳理:
- 初始化服务器
- 进入主循环
2.1) 先从内核中获取到一个 TCP 连接
2.2) 处理 TCP 连接
a) 读取请求并解析
b) 根据请求计算响应
c) 把响应结果写到客户端
仍以简单的 echo sever 为例
1.初始化操作
// 创建一个 socket 对象
private ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException
// 和 UDP 类似,绑定端口号
serverSocket = new ServerSocket(port);
2.1 从内核中获取到一个 TCP 连接
Socket clientSocket = serverSocket.accept();
TCP 连接管理是由操作系统内核来管理的
所谓的连接管理:先描述(通信中的五元组),再组织(使用一个阻塞队列来组织若干个对象)
.
客户端和服务器建立连接的过程,完全由内核来进行负责,应用程序的代码感知不到
当连接建立成功,此时内核已经把这个连接对象放到阻塞队列中了
.
代码中调用的 accept: 就是从阻塞队列中取出一个连接对象(在应用程序中的化身就是 Socket 对象)
若服务器启动之后,没有客户端进行连接,此时代码中调用 accept 就会阻塞,阻塞到真的有客户端建立连接为止
2.2 处理 TCP 连接
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())))
catch (IOException ioException)
ioException.printStackTrace();
clientSocket 里有 getInputStream( ) 方法,和 getOutputStream( ) 方法
此处得到的两个对象都是 “字节流”
InputStream,用来进行读取
OutputStream,用来进行写入
.
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())))
// 此处实现一个 "长连接" 版本的服务器
// 一次连接的处理过程中需要处理多个请求和响应
// 当客户端断开连接时,服务器再调用 readLine 或 write都会触发异常 (IOException),循环结束
while (true)
// a)读取请求并解析
String request = bufferedReader.readLine(); // 客户端发的数据必须是一个按行发送的数据(每一条数据占一行)
// b)根据请求计算响应
String response = process(request);
// c)把响应写回客户端
bufferedWriter.write(response + "\\n"); // 因为客户端要按行来读
bufferedWriter.flush(); // 刷新缓冲区
// 打印日志
System.out.printf("[%s:%d] req: %s; resp: %s\\n",clientSocket.getInetAddress().toString(),
clientSocket.getPort(),request,response);
catch (IOException e)
System.out.printf("[%s:%d] 客户端下线\\n", clientSocket.getInetAddress().toString(),
clientSocket.getPort());
附完整代码:
public class TcpEchoServer
// 创建一个 socket 对象
private ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException
// 和 UDP 类似,绑定端口号
serverSocket = new ServerSocket(port);
public void start() throws IOException
System.out.println("服务器启动..");
while (true)
// 2.1) 先从内核中获取到一个 TCP 连接
Socket clientSocket = serverSocket.accept();
// 2.2) 处理 TCP 连接
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())))
// 此处实现一个 "长连接" 版本的服务器
// 一次连接的处理过程中需要处理多个请求和响应
// 当客户端断开连接时,服务器再调用 readLine 或 write都会触发异常 (IOException),循环结束
while (true)
// a)读取请求并解析
String request = bufferedReader.readLine(); // 客户端发的数据必须是一个按行发送的数据(每一条数据占一行)
// b)根据请求计算响应
String response = process(request);
// c)把响应写回客户端
bufferedWriter.write(response + "\\n"); // 因为客户端要按行来读
bufferedWriter.flush(); // 刷新缓冲区
// 打印日志
System.out.printf("[%s:%d] req: %s; resp: %s\\n",clientSocket.getInetAddress().toString(),
clientSocket.getPort(),request,response);
catch (IOException e)
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 server = new TcpEchoServer(6060);
server.start();
服务器的处理方式:
TCP发送数据时,需要先建立连接,什么时候关闭连接就决定是短连接还是长连接
短连接
一个连接中,客户端和服务器之间只交互一次,交互完毕,就断开连接
每次接收到数据并返回响应后,都关闭连接;也就是说,短连接只能一次收发数据
长连接
一个连接中,客户端和服务器之间交互 N 次,直到满足一定条件再断开连接
不关闭连接,一直保持连接状态,双方不停的收发数据;也就是说,长连接可以多次收发数据
一般认为,长连接效率更高,避免了反复建立连接和断开连接的过程
客户端程序
核心流程:
- 启动客户端(一定不要绑定端口号),和服务器建立连接
- 进入主循环
a)读取用户输入内容
b)构造一个请求发送给服务器
c)读取服务器响应数据
d)把响应数据显示到界面上
private Socket socket = null;
public TcpEchoClient(String serverIP,int serverPort) throws IOException
// 此处实例化 socket 过程,就是在建立 TCP连接
socket = new Socket(serverIP,serverPort);
a) 读取用户输入内容
System.out.print("-> ");
String request = scan.nextLine();
if(request.equals("exit"))
break;
此处的读操作也会阻塞 (用户不一定立刻就输入) —— 阻塞等待用户输入数据
若发生生阻塞,则一直阻塞到用户真的输入了数据为止
b) 构造一个请求发送给服务器
bufferedWriter.write(request + "\\n");
// "\\n" 为了和服务器中的 readLine 相对应
bufferedWriter.flush(); // 刷新缓冲区
此处的 write 发送成功后,服务器就会从 readLine 中返回
注意: 有缓冲区的 IO操作,真正传输数据,需要刷新缓冲区
c) 读取服务器响应数据
String response = bufferedReader.readLine();
在服务器返回数据之前,此处的 readLine 也会阻塞 —— 阻塞等待服务器返回响应
d) 把响应数据显示到界面上
System.out.println(response);
完整代码:
public class TcpEchoClient
private Socket socket = null;
public TcpEchoClient(String serverIP,int serverPort) throws IOException
// 此处实例化 socket 过程,就是在建立 TCP连接
socket = new Socket(serverIP,serverPort);
public void start()
System.out.println("客户端启动...");
Scanner scan = 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.print("-> ");
String request = scan.nextLine();
if(request.equals("exit"))
break;
// b)构造一个请求发送给服务器
bufferedWriter.write(request + "\\n"); // "\\n" 为了和服务器中的 readLine 相对应
bufferedWriter.flush(); // 刷新缓冲区
// c)读取服务器响应数据
String response = bufferedReader.readLine();
// d)把响应数据显示到界面上
System.out.println(response);
catch (IOException e)
e.printStackTrace();
public static void main(String[] args) throws IOException
TcpEchoClient client = new TcpEchoClient("127.0.0.1",6060);
client.start();
多线程改进服务器
上述代码:是一个服务器只能给一个客户端服务,那这个服务器太low了,可以使用多线程对其改进:
代码实现:
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 可以反复快速的调用到 accept ,于是就可以同时处理多个客户端的连接了
while (true)
Socket clientSocket = serverSocket.accept();
// 针对这个连接,单独创建一个线程进行处理
Thread t = new Thread()
@Override
public void run()
// accept 和 processConnection 是并发执行的
processConnection(clientSocket);
;
t.start();
public 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)
// a)读取请求并解析
String request = bufferedReader.readLine(); // 客户端发的数据必须是一个按行发送的数据(每一条数据占一行)
// b)根据请求计算响应
String response = process(request);
// c)把响应写回客户端
bufferedWriter.write(response + "\\n"); // 因为客户端要按行来读
bufferedWriter.flush(); // 刷新缓冲区
// 打印日志
System.out.printf("[%s:%d] req: %s; resp: %s\\n",clientSocket.getInetAddress().toString(),
clientSocket.getPort(),request,response);
catch (IOException e)
e.printStackTrace();
public String process(String request)
return request;
public static void main(String[] args) throws IOException
TcpThreadEchoServer server = new TcpThreadEchoServer(6060);
server.start();
先接收请求,再分配线程
主线程专门负责 accept,其他线程负责和客户端具体沟通
虽然使用多线程解决了 “一个服务器只能给一个客户端服务” 的问题
但是,每次来一个客户端,都要分配一个线程,对于一个服务器来说,随时可能会来大量的客户端,随时也会有大量的客户端断开连接,那么服务器也就需要频繁的创建和销毁线程 (线程的创建和销毁比较轻量,但是也"来不住"量大)
那么,就可以使用 线程池 来解决
线程池再改进:
通过使用线程池,就可以节省频繁创建和销毁线程带来的开销
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 就可以反复快速的调用到 accept ,于是就可以同时处理多个客户端的连接了
while (true)
Socket clientSocket = serverSocket.accept();
// 针对这个连接,单独创建一个线程进行处理
executorService.execute(new Runnable()
@Override
public void run()
processConnection(clientSocket);
);
public 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)
// a)读取请求并解析
String request = bufferedReader.readLine(); // 客户端发的数据必须是一个按行发送的数据(每一条数据占一行)
// b)根据请求计算响应
String response = process(request);
// c)把响应写回客户端
bufferedWriter.write(response + "\\n"); // 因为客户端要按行来读
bufferedWriter.flush(); // 刷新缓冲区
// 打印日志
System.out.printf("[%s:%d] req: %s; resp: %s\\n",clientSocket.getInetAddress().toString(),
clientSocket.getPort(),request,response);
catch (IOException e)
e.printStackTrace();
public String process(String request)
return request;
public static void main(String[] args) throws IOException
TcpThreadPoolEchoServer server = new TcpThreadPoolEchoServer(6060);
server.start();
总结
方法总结:
- ServerSocket API
ServerSocket 是创建TCP服务端Socket的API
- ServerSocket 构造方法