文章目录
前言
本人是一个刚刚上路的IT新兵,菜鸟!分享一点自己的见解,如果有错误的地方欢迎各位大佬莅临指导,如果这篇文章可以帮助到你,劳请大家点赞转发支持一下!
今天分享的内容是TCP流套接字实现的客户端与服务器的通信,一定要理解 DatagramSocket,DatagramPacket 这两个类的作用以及方法,十分有助于你理解服务器,客户端代码。
一、理论准备
Socket套接字是什么
Socket套接字,是由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。基于Socket套接字的网络程序开发就是网络编程。
程序猿👨💻编写网络程序,主要编写的是 应用层的程序代码 ,但是真正想要发送或接收数据,都是要 通过应用层调用传输层 。
因此传输层就为应用层(为我们编写代码)提供了一组api统称为
Socket api。
简单来说,这一组api是提供给咱们 编写网络程序使用的接口 , 用来发送 / 接收网络数据使用的接口 。
TCP协议的特点
特点 | 说明 |
---|---|
有连接 | 刻意保存对端的相关信息 |
可靠传输 | 尽全力将数据传输过去不是百分百成功,自己会知道数据传输是否成功 |
面向字节流 | 以一个字节为基本单位(一个数据可以分成几份 多次发多次收) |
有接收缓冲区,也有发送缓冲区 | 后续文章介绍 |
大小不受限 | 对于要传输的数据大小没有要求 |
全双工 | 一条通信路径,双向通信。(可以同时发送和接收数据) |
二、TCP 流套接字提供的API
ServerSocket API
Server Socket对象可以理解为一个管家,每当有客户端想要连接服务器时,他就会为每个连接进来的服务器提供一个专门伺候他的Socket对象(保姆)
ServerSocket构造方法 | 方法说明 |
---|---|
ServerSocket(int port) | 创建一个服务端 流套接字Socket,并绑定到指定端口 |
ServerSocket方法 | 方法说明 |
---|---|
Socket accept() | 开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待 |
void close() | 关闭此套接字 |
Socket API
Socket对象就是ServerSocket API这个管家分配给每个服务器的保姆
Socket 构造方法 | 方法说明 |
---|---|
Socket(String host, int port) | 创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接 |
Socket 方法 | 方法说明 |
---|---|
InetAddress getInetAddress() | 返回套接字所连接的地址 |
InputStream getInputStream() | 返回此套接字的输入流,可以直接使用这个输入流读取对端发送的数据 |
OutputStream getOutputStream() | 返回此套接字的输出流,可以直接使用这个输出流向对端发送数据 |
三、代码实现请求响应式 客户端服务器
服务器
TCP 流套接字是字节流读取,因此要给每个数据规定一个结束标志,即我们要自定义一个协议,下面就以换行为结束标志当作协议
下面代码中一步一步实现了这三个功能,并配有详细的注释帮你快速理解
// 服务器
public class TcpEchoServer {
// serverSocket 就是管家
// clientSocket 就是伺候每个客户端的保姆
// serverSocket 只有一个. clientSocket 会给每个客户端都分配一个~
private ServerSocket serverSocket = null;
// 指定一个端口号绑定,便于客户端连接
public TcpEchoServer(int port) throws IOException {
this.serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
while (true) {
// 如果没有客户端连接,accept方法会阻塞等待
Socket clientSocket = serverSocket.accept();
// 如果直接调用 processConnection(clientSocket)方法
// 那么此时就会进入该方法,无法及时处理其他连接进来的客户端的请求
// 解决方案:创建新的线程, 用新线程来调用 processConnection
// 每次来一个新的客户端都搞一个新的线程即可!!
// 方法1. 每次都手动创建新线程
/*
Thread t = new Thread(() -> {
try {
processConnection(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
});
t.start();*/
// 方法2. 创建线程池来解决
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.submit(new Runnable() {
@Override
public void run() {
try {
processConnection(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
// 服务器处理客户端请求的主逻辑
private void processConnection(Socket clientSocket) throws IOException {
// 因为对端是通过字节流来发送的数据,因此如果对方发送多条数据,就无法区分数据
// 所以要双方约定好,数据的结束标记,遇到结束标记就代表收到了一个完整的数据
// 此次客户端服务器使用的结束标记为换行 \n
System.out.printf("[%s,%d 客户端上线!\n]",clientSocket.getInetAddress().toString(),clientSocket.getPort());
// try () 这种写法, ( ) 中允许写多个流对象,
// 并且会在try结束后,自动调用对应流的close方法
// 使用 ; 来分割
try (InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()){
Scanner scanner = new Scanner(inputStream);// 读
PrintWriter printWriter = new PrintWriter(outputStream);// 写
// 没有这个 scanner 和 printWriter, 完全可以!! 但是代价就是得一个字节一个字节扣, 找到哪个是请求的结束标记 \n
// 不是不能做, 而是代码比较麻烦.
// 为了简单, 把字节流包装成了更方便的字符流~~
while (true) {
// 如果对端关闭连接,hasNext就会返回false
// 如果对端有数据,hasNext就会返回true
if (!scanner.hasNext()) {
// 读取的流到了结尾了 (对端关闭了)
System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress().toString(),
clientSocket.getPort());
break;
}
// 1. 读取请求
// 直接使用 scanner 读取一段字符串.
// next遇到换行自动停止读取
String request = scanner.next();
// 2. 根据请求计算响应
String response = process(request);
// 3. 把响应写回给客户端. 不要忘了, 响应里也是要带上换行 \n
printWriter.println(response);// 该方法会在放送的同时添加换行添加了换行 \n
//手动刷新缓冲区
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 {
clientSocket.close();
}
}
// 根据请求计算响应的逻辑
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
// 实例化服务器对象
TcpEchoServer tcpEchoServer = new TcpEchoServer(9090);
// 启动主逻辑
tcpEchoServer.start();
}
}
客户端
下面代码中一步一步实现了这四个功能,并配有详细的注释帮你快速理解
// 客户端
public class TcpEchoClient {
private Socket socket = null;
public TcpEchoClient(String serverIp,int port) throws IOException {
// 这个操作相当于让客户端和服务器建立 tcp 连接.
// 这里的连接连上了, 服务器的 accept 就会返回.
this.socket = new Socket(serverIp,port);
}
public void start() {
Scanner scanner = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
Scanner scannerFromSocket = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream)){
while (true) {
// 1. 从键盘上读取用户输入的内容.
System.out.print("-> ");
String request = scanner.next();
// 2. 把读取的内容构造成请求, 发送给服务器.
// 注意, 这里的发送, 是带有换行的!!
printWriter.println(request);
printWriter.flush();// 手动刷新缓冲区
// 3. 从服务器读取响应内容
String response = scannerFromSocket.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 tcpEchoClient = new TcpEchoClient("127.0.0.1", 9090);
// 启动客户端主逻辑
tcpEchoClient.start();
}
}
通信结果:
如何同时多次运行同一个代码
选中第一个即可。
疑惑解答
为什么服务器进程需要手动指定端口号而客户端进程不需要
为什么客户端中的服务器IP与端口号是"127.0.0.1" 与 9090
127.0.0.1 是主机环回地址。主机环回是指地址为 127.0.0.1 的任何数据包都不应该离开计算机(主机),发送它——而不是被发送到本地网络或互联网,它只是被自己“环回”,并且发送数据包的计算机成为接收者。
端口号是9090是因为是随意指定的,当然也有一些特殊端口号被指定分配给了一些牛逼的程序。
为什么服务器Socket对象要关闭,ServerSocket对象却不用,客户端的Socket对象也不用关闭
Socket对象与ServerSocket对象都会产生文件描述符,如果如果文件描述符表满了会产生文件资源泄露的严重bug,那么为什么有的调用,有的没有调用close方法???
缓冲区是什么?为什么要手动刷新缓冲区???
读写硬盘,读写网卡都视为IO操作
因此只有缓冲区满了,才会真正写入网卡。
因此代码中要手动刷新缓冲区,才能保证无论数据大小都可以及时发送。
总结
以上就是今天要分享的内容,本文介绍了Socket套接字,以及使用TCP协议的特点以及TCP流套接字实现的客户端与服务器的通信。网络编程让我愈发感觉到了编程的魅力,也让我领略到了科技的神奇。各位加油!
路漫漫不止修身,也养性。