目录
[4.1 API介绍](#4.1 API介绍)
[4.2 TCP版本服务器代码示例](#4.2 TCP版本服务器代码示例)
[TCP Echo Server(服务端)](#TCP Echo Server(服务端))
[Socket vs ServerSocket](#Socket vs ServerSocket)
[TCP Echo Client(客户端)](#TCP Echo Client(客户端))
书接上文:Java EE:6.网络编程套接字(第一弹)~~
闲聊:
Q1:计算机二级过了!!
恭喜恭喜,但是后面做简历的时候,千万不要往简历上写就好了~~(简历上是减分项)
二级是非常好过的~~
相当于你应聘翻译,你简历上写熟悉 a-z 26个字母的拼写
Q2:驾照需要写吗?
也不是很需要~~
Q3:学校老师说软考越来越重要?
纯属扯淡~~
Q4:软考对简历怎么样??
软考考的东西和你日常开发工作用的东西,没有半点联系
软考虽然比计算机二级难一点,写简历上不是扣分项,但也绝对不是加分项,不加不减~~
软考相当于把计算机中涉及到的各个学科(不止开发和数据结构,还有操作系统、网络、法律条文、软件工程、工程管理......),每个学科挑一些基础的知识点,考你~~
而且软考的题,技巧性非常强,只有你掌握了技巧,哪怕你那些知识啥都不懂,你也能做的七七八八~~
比较务虚,哪怕你考的总分130、140,也不见得你代码就能写的好~~
45+45算过,报名费好像两百多呢~~要是想体会一下考证啥感觉可以去,要是想通过这个写简历上提升竞争力,那还是想多了~~
4.TCP流套接字编程
在 TCP 这里,ServerSocket 和 Socket 都表示 Socket
ServerSocket:专门给服务器用的
Socket:服务器和客户端都会用
那在 TCP 这里不需要一个类来表示发送的基本单位吗??
TCP 的一个核心特点:面向字节流
读写数据的基本单位就是 byte,就不需要一个专门的类了,直接使用字节表示即可
4.1 API介绍
ServerSocket
ServerSocket 是创建 TCP 服务端 Socket 的API
ServerSocket 构造方法:
|------------------------|----------------------------|
| 方法签名 | 方法说明 |
| ServerSocket(int port) | 创建一个服务器流套接字Socket,并绑定到指定端口 |服务器启动,需要先绑定端口号~~
ServerSocket 方法
|-----------------|-----------------------------------------------------------------------|
| 方法签名 | 方法说明 |
| Socket accept() | 开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待 |
| void close() | 关闭此套接字 |TCP 和 UDP 不同,TCP 是"有连接",这里的 accept 就是联通连接的关键操作~~
Socket
Socket 是客户端 Socket,或服务端中接收到客户端建立连接(accept 方法)的请求后,返回的服务端 Socket
不管是客户端还是服务端 Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的
Socket 构造方法:
|------------------------------|------------------------------------------|
| 方法签名 | 方法说明 |
| Socket(String host,int port) | 创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接 |这两个参数是服务器的 IP 和 服务器的端口
Socket 方法:
|--------------------------------|-------------|
| 方法签名 | 方法说明 |
| InetAddress getInetAddress() | 返回套接字所连接的地址 |
| InputStream getInputStream() | 返回此套接字的输入流 |
| OutputStream getOutputStream() | 返回此套接字的输出流 |TCP 这里没有 send / receive 操作,而是借助字节流对象来完成
4.2 TCP版本服务器代码示例
TCP Echo Server(服务端)
javapackage network; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; public class TcpEchoServer { //先创建一个 serverSocket 对象 private ServerSocket serverSocket=null; //这里和 UDP 服务器类似,也是在构造对象的时候,绑定端口号 public TcpEchoServer(int port) throws IOException { serverSocket=new ServerSocket(port); } public void start() throws IOException { System.out.println("启动服务器"); while(true){ //对于 TCP 来说,需要先处理客户端发来的连接 Socket clientSocket=serverSocket.accept(); } } }连接可以理解为"打电话"
一边拨号,另一边要接通才能正常进行通信
而咱们要做的就是"接通"的操作(accept),而"拨号"的操作,则是客户端来完成的~~
Socket vs ServerSocket
比如说我有一次买房子,十一假期的时候,在马路牙子上遇见一个西装革履的小哥~~
后来发现他是个卖房子的销售,而我恰好有买房子的需求,于是就带着我去了售楼部,售楼部里人山人海~~
闲聊:后来才发现,很多售楼部都是人山人海,其中很多都是演员(托)~~
小哥一挥手,来了一个同样西装革履的小姐姐~~OL风格~~
小哥跟我说,她是专业的置业顾问(销售),由她来给你介绍咱们楼盘详细情况~~
而小哥一转身,就消失在茫茫人海中了~~
这个过程中:
小哥:负责在马路牙子上揽客~~外场
就是 ServerSocket 做的工作~~
小姐姐:给客人提供详细的服务~~内场
就是 Socket 做的工作~~
闲聊:
为啥不是小姐姐揽客??
都有都有~~
当时我吧就真的把这个房子给买了~~
但是这是个悲伤的故事,开发商是 恒大~~
2017年,恒大是最牛逼的开放商
但是2019年,恒大就暴雷了,导致很多楼盘,纷纷烂尾~~
当时我已经拿到房子了,只是没有房本~~
后来国家入场了,专门拨款来建设烂尾楼~~
房本也就拿到了~~
所以买房的时候,还是建议大家买"现房",虽然"期房"能便宜十几万,但是很容易烂尾~~亏的就太大了~~
javapackage network; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; public class TcpEchoServer { //先创建一个 serverSocket 对象 private ServerSocket serverSocket=null; //这里和 UDP 服务器类似,也是在构造对象的时候,绑定端口号 public TcpEchoServer(int port) throws IOException { serverSocket=new ServerSocket(port); } public void start() throws IOException { System.out.println("启动服务器"); while(true){ //对于 TCP 来说,需要先处理客户端发来的连接 //通过读写 clientSocket,和客户端进行通信 //如果没有客户端发起连接,此时 accept 就会阻塞 Socket clientSocket=serverSocket.accept(); processConnection(clientSocket); } } //处理一个客户端的连接 //可能要涉及到多个客户端的请求和响应 private void processConnection(Socket clientSocket){ //打印客户端的地址和端口号 System.out.printf("[%s:%d]客户端上线!\n",clientSocket.getInetAddress(),clientSocket.getPort()); //后面读取请求的时候就写到输入流对象,然后写入输出流对象 try(InputStream inputStream=clientSocket.getInputStream(); OutputStream outputStream=clientSocket.getOutputStream()){ //分成三个步骤 while(true){ //1.读取请求并解析 byte[] request=new byte[1024]; InputStream.read(request); //2.根据请求计算响应 //3.返回响应到客户端 } }catch (IOException e){ throw new RuntimeException(e); } } }写到这里时,读取请求并解析的过程可以搞一个字节数组,然后通过 InputStream 来读(文件操作讲过的内容),但是这样写的话,读到的还是字节数组,还是需要我们手动把字节数组转成 String,才方便去进行后续处理,其实我们还有一个更简单的方法:借助 Scanner 来完成读取操作,读的同时还是一个 String
Scanner 这个东西,非常好用,不仅仅可以处理控制台的输入,还能控制文件的输入,还可以控制网络的输入~~
Scanner 构造方法,填入的其实是一个 InputStream 对象~~
1.往 Scanner 里填 System.in →从控制台读
2.往 Scanner 里填 FileInputStream →从文件读
3.往 Scanner 里填 Socket 获取到的 InputStream →从网络读
因此通过 Scanner 获取输入还是非常方便的~~
到目前为止的代码
javapackage network; import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.nio.charset.StandardCharsets; import java.util.Scanner; public class TcpEchoServer { //先创建一个 serverSocket 对象 private ServerSocket serverSocket=null; //这里和 UDP 服务器类似,也是在构造对象的时候,绑定端口号 public TcpEchoServer(int port) throws IOException { serverSocket=new ServerSocket(port); } public void start() throws IOException { System.out.println("启动服务器"); while(true){ //对于 TCP 来说,需要先处理客户端发来的连接 //通过读写 clientSocket,和客户端进行通信 //如果没有客户端发起连接,此时 accept 就会阻塞 Socket clientSocket=serverSocket.accept(); processConnection(clientSocket); } } //处理一个客户端的连接 //可能要涉及到多个客户端的请求和响应 private void processConnection(Socket clientSocket){ //打印客户端的地址和端口号 System.out.printf("[%s:%d]客户端上线!\n",clientSocket.getInetAddress(),clientSocket.getPort()); //后面读取请求的时候就写到输入流对象,然后写入输出流对象 try(InputStream inputStream=clientSocket.getInputStream(); OutputStream outputStream=clientSocket.getOutputStream()){ //针对 InputStream 套了一层 Scanner sc=new Scanner(inputStream); //针对 OutputStream 套了一层 PrintWriter writer=new PrintWriter(outputStream); //分成三个步骤 while(true){ //1.读取请求并解析,可以直接 read,也可以借助 Scanner 来辅助完成 if(!sc.hasNext()){ //没有下一个数据可以读了,连接断开了 System.out.printf("[%s:%d] 客户端下线!\n",clientSocket.getInetAddress(),clientSocket.getPort()); break;//结束循环 } String request=sc.next(); //2.根据请求计算响应 String response=process(request); //3.返回响应到客户端 //按照字节的方式填写 //outputStream.write(response.getBytes()); //也可以使用字符流的方式套一层 //此时 writer 就和之前说的 System.out 是一样的效果了 writer.println(response); //打印日志 System.out.printf("[%s:%d] req:%s,resp:%s\n",clientSocket.getInetAddress(),clientSocket.getPort(), request,response); } }catch (IOException e){ throw new RuntimeException(e); } } private String process(String request) { return request; } public static void main(String[] args) throws IOException { TcpEchoServer server=new TcpEchoServer(9090); server.start(); } }大体流程与 UDP 相比就是多了一个针对连接的处理
连接建立好之后就到了文件 IO 里面的内容一样
拿着 IO 流进行操作,只不过操作的时候,我们稍微套了一下,用 Scanner 和 PrintWriter 这两个类来去代表原来的 inputStream 和 outputStream,主要是因为我们当前是按照字符串来处理文本,这个时候,我们自然而然使用字符流来进行处理,包括说使用 Scanner 来处理,是更方便,更简单的做法
答疑
Q1:一次请求就相当于一次连接吗??
一个连接中,可以包含若干次请求~~
打电话的时候,打通一次电话,可以只说一句话就挂了👇
短链接:一个连接中只有一个请求
也可能收很多句话再挂~~👇
长连接:一个连接中有多个请求
日常开发中,长连接是更常见的情况,因为短链接太消耗性能了,每次发请求都要先建立连接,就像之前我们学过的锁消除一样,给领导汇报工作,应该一口气把该说的话给说完
Q2:意思就是客户端可以发一些文字,然后他回应了,继续发?
发送多次请求,拿到多次响应,不局限于字符串~~(只不过当前咱们这个代码只处理了字符串)
TCP Echo Client(客户端)
javapackage network; import java.io.IOException; import java.net.Socket; public class TcpEchoClient { private Socket socket=null; public TcpEchoClient(String serverIp,int serverPort) throws IOException { //直接把字符串的 IP 地址,设置进来 //类似 127.0.0.1 这种字符串,不需要转换,还是蛮方便的~~ socket=new Socket(serverIp,serverPort); } }创建 socket 对象就会在底层和对端建立 tcp 的连接(所谓连接,就是记录了对端的信息)
服务器的 IP 和 端口 这样的信息,就不需要自己创建变量保存了,直接 TCP 内部就记住了
这一点和 UDP 还不一样,当时我们写 UDP 的时候是自己创建了两个变量来保存服务器的 IP 和 端口 👇
而对于 TCP 来说,你自己创建变量保存也不是不可以,但是没必要~~
到目前为止的代码
javapackage network; 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 { //直接把字符串的 IP 地址,设置进来 //类似 127.0.0.1 这种字符串,不需要转换,还是蛮方便的~~ socket=new Socket(serverIp,serverPort); } public void start(){ Scanner sc=new Scanner(System.in); try(InputStream inputStream=socket.getInputStream(); OutputStream outputStream=socket.getOutputStream()) { //为了使用方便,来个套壳操作 Scanner scanner=new Scanner(inputStream); PrintWriter writer=new PrintWriter(outputStream); //从控制台读取请求,发送给服务器 while(true){ //1.从控制台读取用户输入 String request=sc.next(); //2.发送给服务器 writer.println(request); //3.读取服务器返回的响应 String response=scanner.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(); } }
服务端与客户端的交互
与 UDP 类似,这里的服务端和客户端也存在着配合👇
需要注意的是,服务端的 socket 对象👇
javaprivate ServerSocket serverSocket=null;和客户端的 socket 对象👇
javaprivate Socket socket=null;这俩 socket 对象,绝对不是同一个对象(分别在不同进程中,甚至在不同主机上)
这俩 socket 对象可以理解为两部电话
从 A 的话筒说话,B 的听筒就能听见
从 B 的话筒说话,A 的听筒就能听见
这个过程,A 和 B 在进行一次通信,但 A 和 B 绝对不是同一个对象!!
但是我们运行上述服务端和客户端两个代码之后,会发现客户端已经发了请求,但是服务器并没有收到👇
TCP回显服务端与客户端的交互出现的问题
答疑:客户端服务器,用过一个的端口号,有时候下一次这个端口号用不了了,换一个端口号又可以了,为什么??
??肯定是可以用的,但是前提是你需要把前一个服务器给停掉,因为一个端口只能被一个端口给绑定👇
一个端口只能被一个进程绑定
其实不是服务器没收到,而是客户端的请求根本没有发出去~~
问题就出在客户端的这行代码上👇
java//2.发送给服务器 writer.println(request);这个操作只是把数据放到"发送缓冲区"中,还没有真正写入到网卡里
这个"发送缓冲区"我们之前也说过(嗑瓜子皮儿的那个例子),本质就是一段内存空间
此时我们就需要用 flush 方法来"冲刷缓冲区",将缓冲区中的内容强制发送出去👇
flush刷新缓冲区
答疑:这是 println 的行为吗?如果我不套壳,直接使用 write(),write()也只是放到缓冲区吗??
这是 PrintWriter 的行为,如果不套壳,是可以直接发送的
实际开发中,广泛使用缓冲区这样的概念,flush 这个操作是很关键的~~
上述代码看起来确实能够运行进行通信了,但其实还"暗藏玄机"~~
第一个玄机:
在客户端的下面这行代码中的行为,是自动加上 \n 的👇
java//2.发送给服务器 writer.println(request);如果我们把 println 改成 print 呢?会怎样??👇
第一个玄机:换行符
我们发现,又出现了之前的情况,貌似还是没发出去~~
但实际上,这个时候是发过去了的,服务器是收到了的,但是服务器没有真正处理~~
为啥??问题就在服务器代码中的这一行👇
javaif(!sc.hasNext()){hasNext() 会判定收到的数据中是否包含"空白符"(换行、回车、空格、制表符、翻页符......),遇到空白符,才认为是一个"完整的 next ",在遇到之前,都会阻塞
而下面这行代码👇其实就是在暗暗约定:一个请求 / 响应 使用 \n 作为结束标记,对端读的时候,也是读到 \n 就结束(认为是读到一个完整的请求了)
java//2.发送给服务器 writer.println(request);在 UDP 中,就是以 DatagramPacket 作为单位的
而 TCP 则是以 字节为单位,实际上一个请求,往往是由多个字节构成的,所以引入分隔符是我们标记完整请求边界的一种典型方式,不一定是换行,也可以是其他的、你所约定的任意的字符,总之,我们要通过这种方式,把请求与请求之间的边界给划分好,这个也是 TCP 中的一个关键的环节,后续我们讲 TCP 原理的时候再进一步展开,这里其实涉及到一个"粘包问题"~~
第二个玄机:
在服务器代码中我们涉及到两种 socket ,其中 serverSocket 生命周期贯穿我们整个服务器进程,肯定不需要对它进行关闭,但是 clientSocket 还不关闭,还可以吗??
这里 clientSocket 我们分析一下可以发现,它的生命周期并不是贯穿整个进程,而是跟随这一次连接了👇
但是我们代码中并没有写释放,没写 close() 意味着这里就会产生文件资源泄露,只有打开没有关闭,打开太多就会把文件描述符表耗尽,也就没办法打开新的了
解决方案很简单:在整个方法执行完,在 try - catch 后面加上一个 finally ,补上 close(),然后解决一下异常就行了👇
第三个玄机:
我们知道,一个服务器要能同时给多个客户端提供服务~~
但是我们上面写的代码貌似有点小问题~~
由于 IDEA 默认同一时刻同样的进程只能运行一个,但此时我们需要模拟多个客户端,因此需要设置一下允许运行多个实例👇
接下来我们运行代码看效果👇
第三个玄机:同一时刻只能处理一个客户端
我们发现,问题很明显,同一时刻只能处理一个客户端
这是为什么呢??
其实是代码结构不合理、有 Bug~~
问题就出现在这里:无法同时等待 accept 和等待用户请求
等待用户发请求的时候,没法等 accept
这个时候,有新的客户端连过来了,也无法接通电话
这就导致了同一时刻,服务器只能处理一个客户端的请求了
服务器引入多线程
那怎样兵分两路,分别处理呢??多线程~~
线程的诞生,就是为了这个场景服务的~~
主线程负责进行 accept ,每次 accept 到一个客户端,就创建一个线程,由新线程负责处理客户端的请求
类似于一个负责在外面揽客,一个负责内场的销售小姐姐负责介绍~~
代码改动如下👇
javapublic void start() throws IOException { System.out.println("启动服务器"); while(true){ //对于 TCP 来说,需要先处理客户端发来的连接 //通过读写 clientSocket,和客户端进行通信 //如果没有客户端发起连接,此时 accept 就会阻塞 //主线程负责进行 accept,每次 accept 到一个客户端,就创建一个线程,由新线程负责处理客户端的请求 Socket clientSocket=serverSocket.accept(); Thread t=new Thread(()->{ processConnection(clientSocket); }); t.start(); } }每个新线程负责去处理一个客户端的请求和响应,直至客户端断开了,这个线程也就随之结束了
看看效果👇
使用多线程解决同一时刻只能处理一个客户端的问题
我们在 jconsole.exe 可以看见这个四个线程的状态:一个主线程和三个子线程
其中三个子线程都在这里阻塞👇等待有客户端发送新的请求
javaif(!sc.hasNext()){//我们发现三个子线程都是阻塞在这个位置主线程在这里阻塞👇等待客户端建立连接
javaSocket clientSocket=serverSocket.accept();//而主线程则阻塞在这个位置~~服务器引入线程池
有细心的老铁应该能发现,此时有客户端上来,就创建新的线程,客户端断开,线程就销毁了,是否有办法进一步优化一下呢??
答疑:线程安全??
咱们只是读一下就打印,不涉及到修改同一个变量,因此不涉及线程安全~~
有老铁就想到了,线程池~~
我们可以使用线程池替换掉频繁创建线程的逻辑👇
javapublic void start() throws IOException { System.out.println("启动服务器"); //这种情况一般不会使用 fixedThreadPool,意味着同时处理的客户端连接数目就固定了 ExecutorService executorService= Executors.newCachedThreadPool(); while(true){ //对于 TCP 来说,需要先处理客户端发来的连接 //通过读写 clientSocket,和客户端进行通信 //如果没有客户端发起连接,此时 accept 就会阻塞 //主线程负责进行 accept,每次 accept 到一个客户端,就创建一个线程,由新线程负责处理客户端的请求 Socket clientSocket=serverSocket.accept();//而主线程则阻塞在这个位置~~ //使用多线程的方式来调整 // Thread t=new Thread(()->{ // processConnection(clientSocket); // }); // t.start(); //使用线程池来调整 executorService.submit(()->{ processConnection(clientSocket); }); } }提出问题:不论多线程还是线程池,都意味着一个客户端对应一个线程,那么一个主机上创建的线程数目是否有上限呢??有的!!
正常一个主机创建几千个线程,就已经不容易了,再想创建几万个线程,几十万个线程,是不靠谱的~~
正如我们之前将多线程的时候也说过,线程太多了,也没啥效率了~~
在2000年左右的时候,这个问题叫 C10K 问题:1w个客户端怎么办??
后来随着技术发展,解决 C10K 已经不满足了,引发了 C10M 问题:1000w个客户端怎么办??
其实也有解决办法:IO 多路复用(IO 多路转接)
由于现在的 JVM 中没有原生的 IO 多路复用 API,而是把 API 重新封装了,已经不仅仅是 IO 多路复用了,咱们这里就不展开介绍了,这是隔壁C++需要重点讲的内容
后续我们在专门的精品课部分会详细讲这一块:IO 多路复用、Java NIO、Netty 知名网络框架......
其实当前学的 IO 也叫 BIO(阻塞 IO,Blocking IO)
NIO(非阻塞 IO,NO Blocking IO)
答疑:这个阻塞和 WAITING 有什么区别??
这俩玩楞没有关联关系~~
完整TCP服务端代码
java
package network;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TcpEchoServer {
//先创建一个 serverSocket 对象
private ServerSocket serverSocket=null;
//这里和 UDP 服务器类似,也是在构造对象的时候,绑定端口号
public TcpEchoServer(int port) throws IOException {
serverSocket=new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("启动服务器");
//这种情况一般不会使用 fixedThreadPool,意味着同时处理的客户端连接数目就固定了
ExecutorService executorService= Executors.newCachedThreadPool();
while(true){
//对于 TCP 来说,需要先处理客户端发来的连接
//通过读写 clientSocket,和客户端进行通信
//如果没有客户端发起连接,此时 accept 就会阻塞
//主线程负责进行 accept,每次 accept 到一个客户端,就创建一个线程,由新线程负责处理客户端的请求
Socket clientSocket=serverSocket.accept();//而主线程则阻塞在这个位置~~
//使用多线程的方式来调整
// Thread t=new Thread(()->{
// processConnection(clientSocket);
// });
// t.start();
//使用线程池来调整
executorService.submit(()->{
processConnection(clientSocket);
});
}
}
//处理一个客户端的连接
//可能要涉及到多个客户端的请求和响应
private void processConnection(Socket clientSocket){
//打印客户端的地址和端口号
System.out.printf("[%s:%d]客户端上线!\n",clientSocket.getInetAddress(),clientSocket.getPort());
//后面读取请求的时候就写到输入流对象,然后写入输出流对象
try(InputStream inputStream=clientSocket.getInputStream();
OutputStream outputStream=clientSocket.getOutputStream()){
//针对 InputStream 套了一层
Scanner sc=new Scanner(inputStream);
//针对 OutputStream 套了一层
PrintWriter writer=new PrintWriter(outputStream);
//分成三个步骤
while(true){
//1.读取请求并解析,可以直接 read,也可以借助 Scanner 来辅助完成
if(!sc.hasNext()){//我们发现三个子线程都是阻塞在这个位置
//没有下一个数据可以读了,连接断开了
System.out.printf("[%s:%d] 客户端下线!\n",clientSocket.getInetAddress(),clientSocket.getPort());
break;//结束循环
}
String request=sc.next();
//2.根据请求计算响应
String response=process(request);
//3.返回响应到客户端
//按照字节的方式填写
//outputStream.write(response.getBytes());
//也可以使用字符流的方式套一层
//此时 writer 就和之前说的 System.out 是一样的效果了
writer.println(response);
//这里也需要刷新缓冲区
writer.flush();
//打印日志
System.out.printf("[%s:%d] req:%s,resp:%s\n",clientSocket.getInetAddress(),clientSocket.getPort(),
request,response);
}
}catch (IOException e){
throw new RuntimeException(e);
}finally {
try {
clientSocket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server=new TcpEchoServer(9090);
server.start();
}
}
完整TCP客户端代码
java
package network;
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 {
//直接把字符串的 IP 地址,设置进来
//类似 127.0.0.1 这种字符串,不需要转换,还是蛮方便的~~
socket=new Socket(serverIp,serverPort);
}
public void start(){
Scanner sc=new Scanner(System.in);
try(InputStream inputStream=socket.getInputStream();
OutputStream outputStream=socket.getOutputStream()) {
//为了使用方便,来个套壳操作
Scanner scanner=new Scanner(inputStream);
PrintWriter writer=new PrintWriter(outputStream);
//从控制台读取请求,发送给服务器
while(true){
//1.从控制台读取用户输入
String request=sc.next();
//2.发送给服务器
writer.println(request);
// 加上刷新缓冲区操作,才是真正发送数据
writer.flush();
//3.读取服务器返回的响应
String response=scanner.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();
}
}
长短连接(课件内容)
TCP 发送数据时,需要先建立连接,什么时候关闭连接就决定是短链接还是长连接:
短链接:每次接收到数据并返回响应后,都关闭连接,即是短链接,也就是说,短链接只能一次收发数据
长连接:不关闭连接,一直保持连接状态,双方不停的收发数据,即是长连接,也就是说,长连接可以多次收发数据
对比以上长短连接,两者区别如下:
1.建立连接、关闭连接的耗时:短链接每次请求、响应都需要建立连接,关闭连接;而长连接只需要第一次建立连接,之后的请求、响应都可以直接传输,相对来说,建立连接、关闭连接也是要耗时的,长连接效率更高
2.主动发送请求不同:短链接一般是客户端主动向服务端发送请求;而长连接可以是客户端主动发送请求,也可以是服务端主动发
3.两者的使用场景有不同:短链接适用于客户端请求频率不高的场景,如浏览网页等,长连接适用于客户端于服务端通信频繁的场景,如聊天室、实时游戏等
扩展了解
基于 BIO(同步阻塞 IO)的长连接会一直占用系统资源,对于并发要求很高的服务端系统来说,这样的消耗是不能承受的
由于每个连接都需要不停的阻塞等待接收数据,所以每个连接都会在一个线程中运行
一次阻塞等待对应着一次请求、响应,不停处理也就是长连接的特性:一直不关闭连接,不停的处理请求
实际应用时,服务端一般时基于 NIO(即同步非阻塞 IO)来实现长连接,性能可以极大的提升






