
本篇博客给大家带来的是网络编程的知识点.
🐎文章专栏: JavaEE初阶
🚀若有问题 评论区见
❤ 欢迎大家点赞 评论 收藏 分享
如果你不知道分享给谁,那就分享给薯条.
你们的支持是我不断创作的动力 .
王子,公主请阅🚀
要开心
要快乐
顺便进步
TCP流套接字编程
TCP 的 socket API 和 UDP 的 socket API 差异很大.
但是和的文件操作有密切联系.
TCP中关键的类:
① ServerSocket: 给服务器使用的类,使用这个类来绑定端口号.
② Socket: 既会给服务器用,又会给客户端用.
这两个类抽象了网卡这样的硬件设备.
- ServerSocket
ServerSocket 是创建TCP服务端的API。
ServerSocket 构造方法:

- ServerSocket 方法:
ServerSocket 方法:
如果有客户端, 和服务器建立连接,这个时候服务器的应用程序是不需要做出任何操作(也没有任何感知的),内核直接就完成了连接建立的流程(三次握手).完成流程之后,就会在内核的队列中排队(每个serverSocket 都有一个这样的队列). 应用程序要想和这个客户端进行通信,就需要通过一个 accept 方法把内核队列里已经建立好的连接对象,拿到应用程序中.
- Socket
Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。
不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。
Socket 构造方法:
Socket 方法:
前面的文章说过,TCP是字节流通信方式.
这里的InputStream 和 OutputStream 就是 字节流!!!
可以借助这俩对象,完成数据的"发送"和"接收"通过 InputStream 进行 read 操作,就是"接收".
通过 OutputStream 进行 write 操作, 就是"发送" .
- 利用TCP实现一个回显的程序:
服务器:
java
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 {
private ServerSocket serverSocket;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
while(true) {
//1. 通过accept方法,把内核中已经建立好的连接拿到应用程序.
Socket clientSocket = serverSocket.accept();
//2. 处理一个连接的逻辑.
processConnection(clientSocket);
}
}
public void processConnection(Socket clientSocket) {
//打印一个日志,表示有客户端连上了.
System.out.printf("[%s:%d] 客户端上线!\n",clientSocket.getInetAddress(),clientSocket.getPort());
//读取请求,根据请求计算响应,最后返回响应
//Socket 对象内部包含了两个字节流对象,把InputStream(读取),OutputStream(发送)获取到,完成后续的读写.
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
//一次连接中,可能会涉及多次请求和响应
while(true) {
//1. 读取请求并解析
Scanner scanner = new Scanner(inputStream);
if(!scanner.hasNext()) {
//没有下一行,说明读取完毕,客户端下线
System.out.printf("[%s:%d] 客户端下线",clientSocket.getInetAddress(),clientSocket.getPort());
break;
}
//暗含一个约定: scanner遇到空白符(\n)就返回.
//和客户端那里相呼应.
String request = scanner.next();
//2. 根据请求计算响应
String response = process(request);
//3. 将响应发给客户端,利用PrinterWriter包裹OutputStream
PrintWriter writer = new PrintWriter(outputStream);
//使用PrintWriter的println方法,把响应返回给客户端
//此处用println,而不是print就是为了结尾加上\n,与客户端对应.
writer.println(response);
writer.flush();
//4. 打印日志,表示当前请求的情况.
System.out.printf("[%s:%d] req: %s, resp: %s\n",clientSocket.getInetAddress(),clientSocket.getPort(),
request,response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}

空白符是一类特殊的字符,有 换行, 回车符,空格, 制表符,翻页符, 垂直制表符...等等。客户端发起的请求,此处会以空白符作为结束标记(此处就约定使用 \n)
客户端:
java
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 {
private Socket socket = null;
public TcpEchoClient(String serverIp,int serverPort) throws IOException {
//在创建socket的同时,和服务器建立"连接"此时就得告诉Socket服务器在哪里
//所以需要传服务器 ip 和 端口号.
socket = new Socket(serverIp,serverPort);
}
public void start() {
System.out.println("客户端上线!");
//1. 从控制台读取用户输入的内容.
//2. 把请求发送给服务器
//3. 从服务器读取响应.
//4. 把响应显示到界面上.
Scanner scanner = new Scanner(System.in);
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
while(true) {
//1. 从控制台读取用户输入的内容.
System.out.println("-> ");
String request = scanner.next();
//2. 把请求发送给服务器
PrintWriter writer = new PrintWriter(outputStream);
//使用println是为了让请求后面带上 换行
//是为了和服务器读取请求 scanner.next()呼应.
writer.println(request);
writer.flush();
//3. 读取服务器返回的响应.
Scanner scannerNetwork = new Scanner(inputStream);
String response = scannerNetwork.next();
//4. 把响应显示到界面上.
System.out.println(response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1",9090);
client.start();
}
}
上述代码存在两个问题:
- 服务器程序会出现文件资源泄露的情况.
前面写过的DatagramSocket, ServerSocket 都没写 close,但没关系,因为DatagramSocket 和 ServerSocket, 都是在程序中,只有这么一个对象,生命周期贯穿整个程序的. 但 clientSocket则是在循环中,每次有一个新的客户端来建立连接,都会创建出新的 clientSocket .
如果有很多客户端都来建立连接,此时,就意味着每个连接都会创建 clientSocket. 如果没有手动 close此时这个 socket 对象会占据着文件描述符表的位置,一旦占满,文件资源就泄露了.
那要怎么办呢?
在 processConnection方法末尾加上下面的代码即可.
java
finally {
//手动关闭clientSocket,避免文件资源泄露.
try {
clientSocket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
既然文件资源泄露是因为clientSocket没关,那能不能在processConnection(clientSocket);执行完之后,再把clientSocket关闭呢?
java
public void start() throws IOException {
System.out.println("服务器启动!");
while(true) {
//1. 通过accept方法,把内核中已经建立好的连接拿到应用程序.
Socket clientSocket = serverSocket.accept();
//2. 处理一个连接的逻辑.
processConnection(clientSocket);
clientSocket.close();
}
}
显然是不可以的,如果 在processConnection方法执行过程中遇到问题,抛出异常.那么还没执行到clientSocket.close();循环就结束了.
考虑到抛异常的问题, 于是就将代码改成try with source结构:
java
public void start() throws IOException {
System.out.println("服务器启动!");
while(true) {
//1. 通过accept方法,把内核中已经建立好的连接拿到应用程序.
try(Socket clientSocket = serverSocket.accept()) {
//2. 处理一个连接的逻辑.
processConnection(clientSocket);
}
}
}
- 多个客户端无法同时与服务器进行交互.
先设置允许存在多个客户端.
同时运行两个客户端,在服务端上只能看到第一个客户端. 第二个客户端与服务器没有进行任何交互.
模拟两个客户端连接服务器的过程:
① 第一个客户端过来之后accept 就返回了,得到一个clientSocket.进入了 processConnection. 又进入了一个 while 循环, 这个循环中,就需要反复处理客户端发来的请求数据.如果客户端没发请求,服务器的代码
就会阻塞scanner.hasNext处.
② 此时此刻,第二个客户端也过来建立连接了, 连接是可以成功建立的. 建立成功之后,连接对象就会在内核的队列里等待代码通过 accept. 但是第二个客户端永远不可能执行accept方法,除非第一客户端断开连接.
怎么让两个客户端同时执行呢?
关键就是,让两个循环能够"并发"执行. 这就涉及到前面学过的多线程知识了.
创建新的线程,让新的线程去调用 processConnection
主线程就可以继续执行下一次 accept 了,新线程内部负责 processConnection的循环此时意味着,每次有一个客户端,就都得给分配一个新的线程, 又因为涉及到频繁创建销毁线程. 所以更好的方式是用线程池, 不用当然也可以, 用的话效率高一些.
解法Ⅰ 服务器引入多线程:

java
public void start() throws IOException {
System.out.println("服务器启动!");
while(true) {
//1. 通过accept方法,把内核中已经建立好的连接拿到应用程序.
Socket clientSocket = serverSocket.accept();
Thread t = new Thread(() -> {
//2. 处理一个连接的逻辑.
processConnection(clientSocket);
});
t.start();
}
}
解法Ⅱ 避免频繁创建销毁线程, 服务器引入线程池:

java
//服务器引入线程池
public void start() throws IOException {
System.out.println("服务器");
ExecutorService service = Executors.newCachedThreadPool();
while(true) {
//1. 通过accept方法,把内核中已经建立好的连接拿到应用程序.
Socket clientSocket = serverSocket.accept();
service.submit(new Runnable() {
@Override
public void run() {
processConnection(clientSocket);
}
});
}
}
本篇博客到这里就结束啦, 感谢观看 ❤❤❤
🐎期待与你的下一次相遇😊😊😊