一、ServerSocket API
ServerSocket 是创建TCP服务端Socket的API
ServerSocket 构造方法:
ServerSocket 方法:
二、Socket API
Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket.不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的
Socket 构造方法:
Socket 方法:
三、案例:简单的回显服务
客户端:
package TCPSocket;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoClient {
// 用普通的 socket 即可, 不用 ServerSocket 了
// 此处也不用手动给客户端指定端口号, 让系统自由分配.
private Socket socket = null;
public TcpEchoClient(String serverIP, int serverPort) throws IOException {
// 这里传入的 ip 和 端口号 的含义表示的不是自己绑定, 而是表示和这个 ip 端口建立连接!!
// 调用这个构造方法, 就会和服务器建立连接 (打电话拨号了)
socket = new Socket(serverIP, serverPort);
}
public void start() {
System.out.println("和服务器连接成功!");
Scanner scanner = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream()) {
try (OutputStream outputStream = socket.getOutputStream()) {
while (true) {
// 1. 从控制台读取字符串
System.out.print("-> ");
String request = scanner.next();
// 2. 根据读取的字符串, 构造请求, 把请求发给服务器
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(request);
printWriter.flush(); // 如果不刷新, 可能服务器无法及时看到数据.
// 3. 从服务器读取响应, 并解析
Scanner respScanner = new Scanner(inputStream);
String response = respScanner.next();
// 4. 把结果显示到控制台上.
System.out.printf("req: %s, resp: %s\n", request, response);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);
client.start();
}
}
服务端:
package TCPSocket;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoServer {
// listen => 英文原意 监听~~
// 但是在 Java socket 中是体现不出来 "监听" 的含义的~~
// 之所以这么叫, 其实是 操作系统原生的 API 里有一个操作叫做 listen
// private ServerSocket listenSocket = null;
private ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
// 由于 TCP 是有连接的, 不能一上来就读数据, 而要先建立连接. (接电话)
// accept 就是在 "接电话", 接电话的前提是, 有人给你打了~~, 如果当前没有客户端尝试建立连接, 此处的 accept 就会阻塞.
// accept 返回了 一个 socket 对象, 称为 clientSocket. 后续和客户端之间的沟通, 都是通过 clientSocket 来完成的.
// 进一步讲, serverSocket 就干了一件事, 接电话~~
Socket clientSocket = serverSocket.accept();
//accept:开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该Socket对象建立与客户端的连接,否则阻塞等待
processConnection(clientSocket);//基于该Socket对象(clientSocket)建立与客户端的连接
}
}
//处理连接的方法
private void processConnection(Socket clientSocket) {
System.out.printf("[%s:%d] 客户端建立连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
// 接下来来处理请求和响应:这里的针对 TCP socket 的读写就和文件读写是一模一样的!!
try (InputStream inputStream = clientSocket.getInputStream()) {//返回此套接字的输入流 读
try (OutputStream outputStream = clientSocket.getOutputStream()) {//返回此套接字的输出流 写
// 循环的处理每个请求, 分别返回响应
Scanner scanner = new Scanner(inputStream);
while (true) {
// 1. 读取请求
if (!scanner.hasNext()) {//hasNext()方法判断输入(文件、字符串、键盘等输入流)是否还有下一个输入项,若有,返回true,反之false
System.out.printf("[%s:%d] 客户端断开连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
break;
}
// 此处用 Scanner 更方便. 如果不用 Scanner 就用原生的 InputStream 的 read 也是可以的
String request = scanner.next();//获取输入的字符串
// 2. 根据请求, 计算响应
String response = process(request);
// 3. 把这个响应返回给客户端
// 为了方便起见, 可以使用 PrintWriter 把 OutputStream 包裹一下
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(response);
// 刷新缓冲区, 如果没有这个刷新, 可能客户端就不能第一时间看到响应结果.
printWriter.flush();
System.out.printf("[%s:%d] req: %s, resp: %s\n", clientSocket.getInetAddress().toString(),
clientSocket.getPort(), request, response);
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 此处要记得来个关闭操作.
// 每次连接处理完成之后就要释放资源
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
注意:
每次都要进行 clientSocket.close();当处理完一次连接后就需要释放资源.对于ServerSocket和socket来说不需要自动释放资源,因为它们始终贯穿程序,最终会随着进程退出而释放
上述代码还存在一个严重的问题,服务器端在同一时刻只能处理一个客户端,因为和客户端交互的前提是要先调用accept接收连接,如下:
解决办法: 使用多线程
代码修改如下:
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
// 由于 TCP 是有连接的, 不能一上来就读数据, 而要先建立连接. (接电话)
// accept 就是在 "接电话", 接电话的前提是, 有人给你打了~~, 如果当前没有客户端尝试建立连接, 此处的 accept 就会阻塞.
// accept 返回了 一个 socket 对象, 称为 clientSocket. 后续和客户端之间的沟通, 都是通过 clientSocket 来完成的.
Socket clientSocket = serverSocket.accept();
//为了能使多个客户端与服务器建立连接,要做如下改进:在此,每次 accept 成功, 都创建一个新的线程, 由新线程负责执行这个 processConnection 方法~
//就可以使两个循环并发执行,互不干扰
Thread t = new Thread(() -> {
processConnection(clientSocket);
});
t.start();
}
}
或者:线程池
public void start() throws IOException {
System.out.println("服务器启动!");
ExecutorService pool = Executors.newCachedThreadPool();//创建线程池
while (true) {
// 由于 TCP 是有连接的, 不能一上来就读数据, 而要先建立连接. (接电话)
// accept 就是在 "接电话", 接电话的前提是, 有人给你打了~~, 如果当前没有客户端尝试建立连接, 此处的 accept 就会阻塞.
// accept 返回了 一个 socket 对象, 称为 clientSocket. 后续和客户端之间的沟通, 都是通过 clientSocket 来完成的.
// 进一步讲, serverSocket 就干了一件事, 接电话~~
Socket clientSocket = serverSocket.accept();
// 通过线程池来实现:每次accept成功后,就将此processConnection放入线程池中。
pool.submit(new Runnable() {
@Override
public void run() {
processConnection(clientSocket);
}
});
}
}
拓展:
在未使用多线程解决上述问题前,我们发现我们是可以启动多个客户端,并且多个客户端都显示与服务器连接成功!!如下
但是为什么还是只有一个客户端能与服务器进行通信呢?
在前面我们已经给出答案了,因为和客户端通信交互的前提是要先调用accept接收连接 而在未使用多线程之前,processConnection中的循环不结束我们是无法进入start中的循环,从而不能第二次调用到 accept 方法,所以即使另一个客户端与服务器连接成功,我们仍然不能进行通信!!
PS: 客户端在 new Socket 的时候就已经与服务器建立好连接了,等待服务器调用 accept 方法后才能进行通信
举个例子: 就好比给另一个人打电话,电话已经打通了建立起了连接,但是另一个人如果不接听电话,双方也就不能进行通信