前言: 上篇,我们写了 UDP 版本的简单服务器,本篇通过写一个简单的服务器来了解 TCP 的常用API
 
目录
TCP 版本 (以长连接为例)
面向字节流
服务器程序
仍以简单的 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();
 
 
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();
    }
}
 
 
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 次,直到满足一定条件再断开连接
 不关闭连接,一直保持连接状态,双方不停的收发数据;也就是说,长连接可以多次收发数据
 一般认为,长连接效率更高,避免了反复建立连接和断开连接的过程
客户端程序
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();  // 刷新缓冲区
 
 
c) 读取服务器响应数据
String response = bufferedReader.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();
    }
}
 
 
虽然使用多线程解决了 “一个服务器只能给一个客户端服务” 的问题
 但是,每次来一个客户端,都要分配一个线程,对于一个服务器来说,随时可能会来大量的客户端,随时也会有大量的客户端断开连接,那么服务器也就需要频繁的创建和销毁线程 (线程的创建和销毁比较轻量,但是也"来不住"量大)
 那么,就可以使用 线程池 来解决
线程池再改进:
通过使用线程池,就可以节省频繁创建和销毁线程带来的开销
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 构造方法
 
| 方法签名 | 方法说明 | 
|---|---|
| ServerSocket(int port) | 创建一个服务端流套接字Socket,并绑定到指定端口 | 
- ServerSocket 方法:
 
| 方法签名 | 方法说明 | 
|---|---|
| Socketaccept() | 开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待 | 
| void close( ) | 关闭此套接字 | 
- Socket API
 
Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端
 Socket
 不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据
 的
- Socket 构造方法
 
| 方法签名 | 方法说明 | 
|---|---|
| Socket(String host, intport) | 创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接 | 
- Socket 方法:
 
| 方法签名 | 方法说明 | 
|---|---|
| InetAddress getInetAddress( ) | 返回套接字所连接的地址 | 
| InputStream getInputStream( ) | 返回套接字所连接的输入流 | 
| OutputStream getOutputStream( ) | 返回此套接字的输出流 | 
UDP & TCP 区别
| UDP 核心类 | TCP 核心类 | 
|---|---|
| ①DatagramSocket | ①ServerSocket | 
| ②DatagramPacket | ②Socket | 
在UDP中,必须使用 receive 和 send,传输数据的基本单位是 DatagramPacket 对象 【面向数据报】
 在TCP中,必须要 read 和 write,传输数据的基本单位是 字节 【面向字节流】
| UDP | TCP | |
|---|---|---|
| 是否连接 | 无连接 | 面向连接 | 
| 是否可靠 | 不可靠传输,不使用流量控制和拥塞控制 | 可靠传输,使用流量控制和拥塞控制 | 
| 传输方式 | 面向数据报 | 面向字节流 | 










