不像 UDP 有 DatagramPacket 是专门的“UDP 数据报”,TCP 没有专门的“TCP 数据报”
专门给服务器使用的 socket
对象
方法签名 | 方法说明 | |
---|---|---|
ServerSocket(int port) 创建⼀个服务端流套接字 Socket,并绑定到指定端⼝ | 创建⼀个服务端流套接字 Socket,并绑定到指定端⼝ |
方法签名 | 方法说明 | |
---|---|---|
Socket accept() | 开始监听指定端⼝(创建时绑定的端⼝),有客⼾端连接后,返回⼀个服务端 Socket 对象,并基于该 Socket 建⽴与客⼾端的连接,否则阻塞等待 | |
void close() | 关闭此套接字 |
既会给客户端使用,又会给服务器使用
方法签名 | 方法说明 | |
---|---|---|
Socket(String host, int port) | 创建⼀个客⼾端流套接字 Socket,并与对应 IP 的主机上,对应端⼝的进程建⽴连接 |
方法签名 | 方法说明 | |
---|---|---|
InetAddress getInetAddress() | 返回套接字所连接的地址 | |
InputStream getInputStream() | 返回此套接字的输⼊流 | |
OutputStream getOutputStream() | 返回此套接字的输出流 |
InputStream
和 OutputStream
称为“字节流”
TCP Socket
来说,也是完全适用的Server Socket
对象,起到“遥控网卡”的作用import java.io.IOException;
import java.net.ServerSocket;
public class TcpEchoServer {
private ServerSocket serverSocket= null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
}undefinedundefinedsocket
对象创建的时候,就指定一个端口号 port
,作为构造方法的参数JVM
就会调用系统的 Socket API
,完成“端口号-进程”之间的关联动作
- 这样的操作也叫“绑定端口号”(系统原生 API
名字就叫 bind
)
- 绑定好了端口号之后,就明确了端口号和进程之间的关联关系Socket
对象来完成)
- 因为端口号是用来区分进程,收到数据之后,明确说这个数据要给谁,如果一个端口号对应到多个进程,那么就难以起到区分的效果
- 如果有多个进程,尝试绑定一个端口号,只有一个能绑定成功,后来的都会绑定失败public void start() throws IOException {
while(true) {
//建立连接
Socket clientSocket = serverSocket.accept();
processConnection(clientSocket);
}
}
accept
操作,是内核已经完成了连接建立的操作,然后才能够进行“接通电话”
- accept
相当于是针对内核中已经建立好的连接进行“确认”动作accept
的返回对象是 Socket
,所以还需要创建一个 clientSocket
来接收返回值
- clientSocket
和 serverSocket
这两个都是 Socket
,都是“网卡的遥控器”,都是用来操作网卡的。但是在 TCP
中,使用两个不同的 Socket
进行表示,他们的分工是不同的,作用是不同的
- serverSocket
就相当于是卖房子的销售,负责在外面揽客
- clientSocket
相当于是售楼部里面的置业顾问,提供“一对一服务”针对一个连接,提供处理逻辑
InputStream
对象用来读取数据,创建一个 OutputStream
对象while
死循环中完成客户端针对请求的响应处理TCP
是全双工的通信,所以一个 Socket
对象,既可以读,也可以写clientSocket
对象拿出里面的 InputStream
和 OutputStream
,我们就既能读,也能写了通过 inputStream.read()
读取请求,但如果直接这样读就不方便,读到的还是二进制数据
Scanner
包装一下 InputStream
,这样就可以更方便地读取这里的请求数据了//针对一个连接,提供处理逻辑
private void processConnection(Socket clientSocket) {
//打印客户端信息
System.out.printf("[%s:%d] 客户端上线!",clientSocket.getInetAddress(),clientSocket.getPort());
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()){
Scanner scanner = new Scanner(inputStream);
//使用 Scanner 包装一下 InputStream,就可以更方便地读取这里的请求数据了
while(true) {
// 1. 读取请求并解析
if(!scanner.hasNext()){
//如果 scanner 无法读取数据,说明客户端关闭了连接,导致服务器这边读取到 “末尾”
break;
}
// 2. 根据请求计算响应
// 3. 把响应写回给客户端
}
}catch (IOException e){
e.printStackTrace();
}
System.out.printf("[%s:%d] 客户端下线!",clientSocket.getInetAddress(),clientSocket.getPort());
}scanner
无法读取出数据时(scanner
没有下一个数据了),说明客户端关闭了连接,导致服务器这边读到了末尾,就进行 break
- 在这个判断的外面(try/catch
外面)加上日志,当数据读完后 break
了,就打印日志由于是回显服务器,所以请求就是响应,process
就是直接 return request
//针对一个连接,提供处理逻辑
private void processConnection(Socket clientSocket) {
//打印客户端信息
System.out.printf("[%s:%d] 客户端上线!",clientSocket.getInetAddress(),clientSocket.getPort());
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()){
Scanner scanner = new Scanner(inputStream);
//使用 Scanner 包装一下 InputStream,就可以更方便地读取这里的请求数据了
while(true) {
// 1. 读取请求并解析
if(!scanner.hasNext()){
//如果 scanner 无法读取数据,说明客户端关闭了连接,导致服务器这边读取到 “末尾”
break;
}
// 2. 根据请求计算响应
String response = process(request);
// 3. 把响应写回给客户端
}
}catch (IOException e){
e.printStackTrace();
}
System.out.printf("[%s:%d] 客户端下线!",clientSocket.getInetAddress(),clientSocket.getPort());
private String process(String request) {
return request;
}
}
InputStream
里面的数据//针对一个连接,提供处理逻辑
private void processConnection(Socket clientSocket) {
//打印客户端信息
System.out.printf("[%s:%d] 客户端上线!",clientSocket.getInetAddress(),clientSocket.getPort());
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()){
Scanner scanner = new Scanner(inputStream);
//使用 Scanner 包装一下 InputStream,就可以更方便地读取这里的请求数据了
PrintWrite printWriter = new PrintWriter(outputStream);
while(true) {
// 1. 读取请求并解析
Scanner scanner = new Scanner(inputStream);
if(!scanner.hasNext()){
//如果 scanner 无法读取数据,说明客户端关闭了连接,导致服务器这边读取到 “末尾”
break;
}
// 2. 根据请求计算响应
String response = process(request);
// 3. 把响应写回给客户端
printWriter.println(response);
}
}catch (IOException e){
e.printStackTrace();
}
System.out.printf("[%s:%d] 客户端下线!",clientSocket.getInetAddress(),clientSocket.getPort());
private String process(String request) {
return request;
}
}
\n
”
- 我们在刚才在使用 scanner
读取请求的时候,隐藏了一个条件——请求是以“空白符”(空格、回车、制表符、垂直制表符、翻页符......)结尾,否则就会在 next()
或者 hasNext()
那里发生阻塞,这样就没法读取到数据了
- 因此此处约定,使用“\n
”作为请求和响应的结尾标志TCP
是字节流的,读写方式存在无数种可能,就需要有办法区分出,从哪里到哪里是一个完整的请求
- 此处就可以引入分隔符来区分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= null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
while(true) {
//建立连接
Socket clientSocket = serverSocket.accept();
processConnection(clientSocket);
}
}
//针对一个连接,提供处理逻辑
private void processConnection(Socket clientSocket) {
//打印客户端信息
System.out.printf("[%s:%d] 客户端上线!",clientSocket.getInetAddress(),clientSocket.getPort());
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()){
Scanner scanner = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream);
//使用 Scanner 包装一下 InputStream,就可以更方便地读取这里的请求数据了
while(true) {
// 1. 读取请求并解析
if(!scanner.hasNext()){
//如果 scanner 无法读取数据,说明客户端关闭了连接,导致服务器这边读取到 “末尾”
break;
}
String request = scanner.next();
// 2. 根据请求计算响应
String response = process(request);
// 3. 把响应写回给客户端
printWriter.println(response);
System.out.printf("[%s:%d] req=%s; resp=%s\n", clientSocket.getInetAddress(),clientSocket.getPort());
}
}catch (IOException e){
e.printStackTrace();
}
System.out.printf("[%s:%d] 客户端下线!",clientSocket.getInetAddress(),clientSocket.getPort());
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
虽然把服务器代码编写的差不多了,但还存在三个非常严重的问题,都会导致严重的 bug
但需要结合后面客户端的代码进行分析
首先创建一个 Socket 对象,来进行网络通信,再创建构造方法
import java.io.IOException;
import java.net.Socket;
public class TcpEchoClient {
private Socket socket = null;
public TcpEchoClient(String serverIp, int serverPort) throws IOException {
socket = new Socket(serverIp,serverPort);
}
}
IP
和端口号
- 这里可以直接填入一个 String
类型的 IP
,不用像前面 UDP
那样还需要手动转换socket
里面的 InputStream
和 OutputStream
,再进行 while
循环public void start() {
System.out.println("客户端启动!");
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
Scanner scanner = new Scanner(inputStream);
Scanner scannerIn = new Scanner(System.in);
PrintWriter printWriter = new PrintWriter(outputStream);
while(true){
//1. 从控制台读取数据
System.out.println("-> ");
String request = scannerIn.next();
//2. 把请求发送给服务器
printWriter.println(request);
//3. 从服务器读取响应
if(!scanner.hasNext()){
break;
}
String response = scanner.next();
//4. 打印响应结果
System.out.println(response);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}Scanner
包装一下 InputStream
,这样就可以更方便地读取这里的请求数据了PrintWriter
对象,获取到 OutputStream
,方便后续对数据进行打印scannerIn
对象,用来读取从控制台输入的数据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 = new Socket(serverIp,serverPort);
}
public void start() {
System.out.println("客户端启动!");
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
Scanner scanner = new Scanner(inputStream);
Scanner scannerIn = new Scanner(System.in);
PrintWriter printWriter = new PrintWriter(outputStream);
while(true){
//1. 从控制台读取数据
System.out.println("-> ");
String request = scannerIn.next();
//2. 把请求发送给服务器
printWriter.println(request);
//3. 从服务器读取响应
if(!scanner.hasNext()){
break;
}
String response = scanner.next();
//4. 打印响应结果
System.out.println(response);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1",9090);
client.start();
}
}
PrintWriter
这样的类,以及很多 IO
流中的类,都是“自带缓冲区”的
- 进行文件/网络操作,都是 IO 操作,IO 操作本身是一种耗时比较多,开销比较大的操作。耗时比较多的操作频繁进行,就会影响程序执行效率,所以我们可以引入“缓冲区”,减少 IO 的次数,从而提高效率
- 引入“缓冲区”之后,进行写入操作,不会立即触发 IO,而是先放到内存缓冲区中,等到缓冲区里攒了一波之后,再统一进行发送flush
操作,主动“刷新缓冲区”
- flush 的原意为“冲刷”,类似于冲厕所改为:
// 客户端
printWriter.println(request);
printWriter.flush();
// 服务器
printWriter.println(response);
printWriter.flush();
ServerSocket
和 DatagramPacket
,它们的生命周期都是跟随整个进程的,和进程同生死,进程关了之后他俩对应的资源也释放了clientSocket
并非如此,它是“连接级别”的数据,随着客户端断开连接了,这个 Socket
也就不再使用了,但资源是不释放的
- 即使是同一个客户端,断开之后,重新连接,也是一个新 Socket
,和旧的 Socket
不是同一个了
- 因此,这样的 Socket
就应该主动关闭掉,避免文件资源泄露改后:
把 close
加到 finally
里面,把日志前移(不然释放之后日志就打印不出来了)
private void processConnection(Socket clientSocket) throws IOException {
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()){
...
while(true) {
...
}
}catch (IOException e){
e.printStackTrace();
}finally {
System.out.printf("[%s:%d] 客户端下线!\n",clientSocket.getInetAddress(),clientSocket.getPort());
clientSocket.close();
}
}
GC
释放的是内存资源,此处讨论的“文件资源泄露”是针对文件描述符的
GC
回收了,也是会自动执行 close
的,但是由于 GC
过程是不可逆的(不知道 GC
什么时候发生,也不知到这次 GC
是否能释放掉你这个对象)processConnect
内部的 while
循环,无法跳出accept
processConnect
的循环就结束了,于是外层的循环就可以执行 accept
了,也是就可以处理第二个客户端之前积压的请求数据了while
循环的写法,导致进入里层 while
的时候,外层 while
就无法执行了while
改成一重 while
,分别进行执行——使用多线程改后:
public void start() throws IOException {
while(true) {
//建立连接
Socket clientSocket = serverSocket.accept();
Thread t = new Thread(() -> {
try {
processConnection(clientSocket);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
t.start();
}
}
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。