赞
踩
目录
步骤二: 调用processConnection方法来处理客户端发送的连接
①通过参数传入的clientSocket来获取输入、输出流对象,此处采用的是try()的方法来关闭流对象
④通过OutputStream+PrintWriter来发送response字符串给客户端
步骤1:通过socket来获取到与服务端进行数据交互的inputStream和OutputStream
步骤3:把读取到的request以流的形式发送给服务端,获取响应
TCP协议的具体介绍,已经在上一篇文章当中提到了。
同时,上一篇文章也手写了一个Udp协议。
(2条消息) 认识UDP、TCP协议_革凡成圣211的博客-CSDN博客https://blog.csdn.net/weixin_56738054/article/details/128709206?spm=1001.2014.3001.5501 在这一篇文章当中,udp的客户端和服务端之间的通信,使用的时DatagramSocket和DatagramPacket这两个api来完成传递信息的。DatagramSocke负责发送和接收消息,DatagramPacket用来传输报文
而在TCP协议当中,提供的API主要是下面两个类:
ServerSocket:专门给服务端使用的socket。
Socket:既可以提供给客户端使用,也可以给服务端使用。
构造方法:
方法签名 | 方法说明 |
ServerSocket(int port) | 创建一个服务端嵌套字,并且指定服务端所占用的进程 |
成员方法: accept
方法签名 | 方法说明 |
Socket accept() | accept方法,用来表示建立客户端与服务端的连接 前面一篇文章当中,我们提到了,TCP是"有连接"的协议,TCP客户端与服务端一定要建立连接,才可以互相发送消息。因此这个accept方法,返回的socket对象,服务端就是通过这个socket对象和客户端进行通信的。 如果服务端没有收到socket对象,那么就会阻塞等待,无法进行通信。 |
对于服务端来说,是由accept()方法返回的的,返回的socket对象用于和客户端进行通信。
构造方法
对于客户端来说,在客户端的构造方法当中,需要构造对象的时候,指定一个IP以及端口号
这个IP以及端口号都是服务端的
两个常用普通方法
方法签名 | 方法说明 |
getInputStream() | 通过socket对象,获取到内部的输入流对象 |
getOutputStream() | 通过socket对象,获取到内部的输出流对象 |
需要在TcpEchoServer内部封装一个属性,这个属性是ServerSocket。
在构造方法当中,需要指定ServerSocket占用哪个端口号,此端口号就是服务端的端口号
代码实现:
- /**
- * @author 25043
- */
- public class TcpEchoServer {
-
- /**
- * 用于Tcp客户端与服务端通信
- * 的socket对象
- */
- private ServerSocket serverSocket;
-
-
-
- public TcpEchoServer(int port) throws IOException {
- //指定服务端进程占用的端口号
- serverSocket=new ServerSocket(port);
- }
- //使用clientSocket来与客户端进行交流
- Socket clientSocket=serverSocket.accept();
此处serverSocket.accept()方法的效果是接收客户端发送的连接。
一个客户端对应一个accept方法获取的clientSocket
由于Socket代表一个文件,任何一个文件会对应进程当中的一个文件描述符表。
也就是这个socket会占用额外的磁盘空间,因此当客户端和服务端通信结束之后,需要把这个连接释放掉(释放的操作,会在后面提到)
客户端在构造socket对象的时候,就会指定服务端的IP以及端口号。
客户端如果想与服务端通信,一定需要建立连接!!因此,如果发服务端启动之后,没有客户端发送连接过来,那么服务端就会在accept()方法这里阻塞等待。
因此,在服务端当中,通信的逻辑应当是这样的:
需要注意的是:一个Socket对应的是一个客户端发送的连接,但是在processConnection内部额有可能涉及处理多个客户端连接的步骤:也就是在Tcp协议当中,服务端与客户端的关联关系为一对多。
但是,以下的代码,先来体验一下单线程的模式。最后,将会演示一个多线程版本。
- //获取clientSocket当中输入、输出流对象
- try(InputStream inputStream= clientSocket.getInputStream();
-
-
- OutputStream outputStream= clientSocket.getOutputStream()) {
以下②③④一共3个步骤,需要在while(true)循环内部不断进行,直到scanner无法读取到内容了
- //2、根据请求构造响应
-
-
- //通过scanner.next()的方式来读取,需要注意的是
-
-
- //scanner遇到空格/换行符/其他空白字符会停止读取
-
-
- //但是,读取的结果里面不会包含这三种符号
-
-
- String request=scanner.next();
- //回写的内容
- String response="服务端已经响应:"+request;
- //使用PrintWriter来发送outputStream
-
- PrintWriter printWriter=new PrintWriter(outputStream);
-
- printWriter.println(response);
-
- //刷新缓冲区,保证当前数据一定会被发送出去
-
- printWriter.flush();
在finally代码块当中,关闭连接,释放文件描述符表。
- finally {
- try {
- //关闭此次连接
- clientSocket.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
整体服务端代码(单线程版)
- /**
- * @author 25043
- */
- public class TcpEchoServer {
-
- /**
- * 用于Tcp客户端与服务端通信
- * 的socket对象
- */
- private ServerSocket serverSocket;
-
-
-
- public TcpEchoServer(int port) throws IOException {
- //指定服务端进程占用的端口号
- serverSocket=new ServerSocket(port);
- }
-
- /**
- * 启动服务端
- */
- public void start() throws IOException {
- System.out.println("启动服务端");
- while (true){
- //使用clientSocket来与客户端进行交流
- Socket clientSocket=serverSocket.accept();
- processConnection(clientSocket);
- }
- }
-
- /**
- * 处理客户端发送的连接
- * 客户端发送的连接@param clientSocket
- */
- private void processConnection(Socket clientSocket) {
- //输出客户端的IP以及端口号
- System.out.println("客户端已经上线!客户端的IP是:"
- +clientSocket.getInetAddress()+
- ";客户端的端口是:"+clientSocket.getPort());
- //获取clientSocket当中输入、输出流对象
- try(InputStream inputStream= clientSocket.getInputStream();
- OutputStream outputStream= clientSocket.getOutputStream()) {
- //使用while循环,处理多个请求+响应
- while (true){
- //1、通过scanner来读取inputStream
- Scanner scanner=new Scanner(inputStream);
- //读取完毕之后,直接返回:
- if(!scanner.hasNext()){
- System.out.println("客户端已经下线!客户端的IP是:"
- +clientSocket.getInetAddress()+
- ";客户端的端口是:"+clientSocket.getPort());
- //退出循环
- break;
- }
- //2、根据请求构造响应
- //通过scanner.next()的方式来读取,需要注意的是
- //scanner遇到空格/换行符/其他空白字符会停止读取
- //但是,读取的结果里面不会包含这三种符号
- String request=scanner.next();
- //构造回写的内容response
- String response="服务端已经响应:"+request;
- //使用PrintWriter来发送outputStream
- PrintWriter printWriter=new PrintWriter(outputStream);
- printWriter.println(response);
- //刷新缓冲区,保证当前数据一定会被发送出去
- printWriter.flush();
-
- }
- } catch (IOException e) {
- e.printStackTrace();
- }finally {
- try {
- clientSocket.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
-
- }
此处,需要一个socket,指定服务端的IP+端口号
- private Socket socket;
-
- public TcpEchoClient(String serverIp,int port) throws IOException {
- //指定服务端的ip+端口号
- socket=new Socket(serverIp,port);
- }
如果客户端想和服务端进行通信,就一定需要指定服务端的端口号。因为TCP是有连接的协议,不允许在没有建立连接的情况下面发送消息。
当socket对象被创建之后,也就意味着客户端成功与服务端建立连接。
客户都安的socket创建之后的一瞬间,服务端的accept方法已经接收早到就客户端的socket对象。
需要注意的是,从客户端的socket获取的InputStream和OutputStream都是相对于客户端来进行输入/输出操作的。
- //1.客户端从键盘上面读取内容
- String request=input.next();
- //2.把读取到的内容构造成请求,发送到客户端
- PrintWriter printWriter=new PrintWriter(outputStream);
- printWriter.println(request);
- //加上flush,刷新缓冲区
- printWriter.flush();
可以看到,此处,使用的是printWriter.println(request)来发送字符串的
但是,是否可以替换成print,也就是不采用\n呢?
答案是,不可以:原因:
在服务端当中,是使用Scanner scanner=input.next()来接收客户端发送的内容的:
回顾一下scanner.next()在什么时候会停止读取,那就是在读取到\n或者空格或者空白字符的时候,就会停止读取。因此,此处客户端发送的内容当中,一定要带有\n,才可以确scanner.next()停止读取。
- //读取服务器响应
- Scanner scanner=new Scanner(inputStream);
- String response= scanner.next();
- //把响应的内容回显到界面上面
- System.out.println(response);
整体客户端代码:
- /**
- * Tcp客户端
- * @author 25043
- */
- public class TcpEchoClient {
- private Socket socket;
-
- public TcpEchoClient(String serverIp,int port) throws IOException {
- //指定服务端的ip+端口号
- System.out.println("服务端已经指定端口号"+System.currentTimeMillis());
- socket=new Socket(serverIp,port);
-
- }
- public void start(){
- System.out.println("客户端启动!");
- Scanner input=new Scanner(System.in);
- //此处获取到的输入流、输出流对象,都是已经跟客户端建立了联系的
- try (InputStream inputStream= socket.getInputStream();
- OutputStream outputStream= socket.getOutputStream()){
- while (true){
- //1.客户端从键盘上面读取内容
- String request=input.next();
- //2.把读取到的内容构造成请求,发送到客户端
- PrintWriter printWriter=new PrintWriter(outputStream);
-
-
- printWriter.println(request);
- //加上flush
- printWriter.flush();
- //读取服务器响应
- Scanner scanner=new Scanner(inputStream);
- String response= scanner.next();
- //把响应的内容回显到界面上面
- System.out.println(response);
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
-
- public static void main(String[] args) throws IOException {
- //指定服务端的ip以及端口号
- TcpEchoClient tcpEchoClient=new TcpEchoClient("127.0.0.1",9090);
- tcpEchoClient.start();
- }
-
- }
单线程TCP存在问题分析:
TCP的服务端的核心代码就是start方法
当服务端启动之后,如果有客户端与服务端建立联系,那么accept方法就会返回一个socket对象,服务端使用这个socket对象与客户端进行通信。
紧接着,服务端在processConnection方法当中,针对客户端发送过来的clientSocket进行不断地使用while循环,调用scanner.next方法进行读取操作。
那么,也就意味着,只要客户端不下线,服务端就会一直停留在这个processConnection的while循环当中。
由于在上述的代码当中,服务端的代码是单线程的。因此,服务端无法从processConnection方法当中离开,即使其他的客户端想再次给服务端建立连接,服务端也accept不到。
但是,如果在processConnection当中不采用while循环,那么这样可以吗?
也是不行的,原因:
如果不使用while循环,那么,服务端只会读取一次客户端发送的请求,也就是调用一次scanner.next方法,然后服务端就会把连接给close掉了。
那么这个客户端如果想再次建立连接,就需要重新获取连接,也就是再次new一个Socket对象。
但是,在上面客户端的代码当中,调用客户端构造方法的时候,只创建了一个连接,也就是一个socket。
因此,如果客户端想要再次发送消息,就没有办法发送了。
客户端代码:
大部分的代码写法都和单线程的一致,唯一的区别就在于,每调用一次processConnection方法需要创建新一个线程来执行。
这样,每获取到一个clientSocket,就会创建一个新的线程t来执行processConnection方法。
即使线程t出现了异常情况,无法结束运行,也不会影响主线程不断接收新的客户端连接。
代码实现:
- /**
- * 启动服务端(多线程版)
- */
- public void start() throws IOException {
- System.out.println("启动服务端");
- while (true){
- //使用clientSocket来与客户端进行交流
- Socket clientSocket=serverSocket.accept();
- Thread t=new Thread(new Runnable() {
- @Override
- public void run() {
- processConnection(clientSocket);
- }
- });
- t.start();
- }
- }
以上代码,在客户端数量不大的情况下面,是可以行得通的
但是,如果客户端数量比较庞大,并且线程的创建、销毁工作也是开销比较大的,因此,可以考虑使用线程池来处理processConnection方法,这样就可以减少了线程不断创建、销毁带来的开销。
代码实现:
- private ExecutorService threadPool= Executors.newCachedThreadPool();
- /**
- * 启动服务端(多线程版)
- */
- public void start() throws IOException {
- System.out.println("启动服务端");
- while (true){
- //使用clientSocket来与客户端进行交流
- Socket clientSocket=serverSocket.accept();
- //往线程池当中提交任务
- threadPool.submit(() -> processConnection(clientSocket));
- }
- }
①客户端与服务器建立连接
②发送一次请求
③读取响应
④关闭连接
下次通信,就需要再一次建立连接。可以看到,短连接每一次通信只会建立一次连接。
①客户端与服务端建立连接
②客户端发送消息
③读取响应
④根据需求,尝试再次发送消息(也就是回到2)
⑤重复②到④之间若干次,再决定是否断开连接
可以看到,长连接的特点就是一次连接多次发送消息。而短连接,就是一次连接只可以发送一次请求。看似长连接的复用性更高,但是其实也不一定说要使用长连接的策略才好,需要结合具体的应用场景。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。