【Java】面向TCP接口的网络编程

一.基本通信模型

在这里插入图片描述

  • TCP传输有连接;
  • TCP传输面向字节流(Stream),无需构造数据报(Datagram)。

二. API

ServerSocket

ServerSocket 是创建TCP服务端Socket的API。
ServerSocket 构造⽅法:

⽅法签名⽅法说明
ServerSocket(int port)创建⼀个服务端流套接字Socket,并绑定到指定端⼝

ServerSocket ⽅法:

⽅法签名⽅法说明
Socket accept()开始监听指定端⼝(创建时绑定的端⼝),有客⼾端连接后,返回⼀个服务端Socket对象,并基于该Socket建⽴与客⼾端的连接,否则阻塞等待
void close()关闭此套接字

此处一定要注意的是:返回的服务端Socket对象并不是客户端Socket对象本体,而是相当于接听电话的关系,客户端拨通后,服务器连接。

Socket

Socket 是客⼾端Socket,或服务端中接收到客⼾端建⽴连接(accept⽅法)的请求后,返回的服务端Socket。
不管是客⼾端还是服务端Socket,都是双⽅建⽴连接以后,保存的对端信息,即⽤来与对⽅收发数据的。

Socket 构造⽅法:

⽅法签名⽅法说明
Socket(String host, int port)创建⼀个客⼾端流套接字Socket,并与对应IP的主机上,对应端⼝的进程建⽴连接

Socket ⽅法:

⽅法签名⽅法说明
InetAddress getInetAddress()返回套接字所连接的地址
InputStream getInputStream()返回此套接字的输⼊流
OutputStream getOutputStream()返回此套接字的输出流
void close()关闭此套接字

三. 回显服务器/客户端示例

客户端

在编写客户端程序时,首先要考虑清楚通信模型,即如何通过字节流来进行通信,这里就要使用Socket类中的getInputStream()getOutputStream()方法来进行构建,同时还需要从终端读取请求,那还需要构建一个从终端读取请求的功能。
那么所搭建的基本框架如下:

try(InputStream inputStream=socket.getInputStream();
    OutputStream outputStream=socket.getOutputStream()){
     Scanner scannerConsole=new Scanner(System.in); //从终端读取请求
     Scanner scannerNetwork=new Scanner(inputStream);//从输入流读取服务器返回的响应
     PrintWriter writer=new PrintWriter(outputStream);//通过输出流向服务器发送请求
     while(true){
     //1.从终端读取请求
     //2.把请求发送给服务器
     //3.从服务器读取响应
     //4.打印响应
     }
}catch (IOException e){
     throw new RuntimeException(e);
}

搭建好框架后,与UDP通信类似,这里客户端要做的也是四步:从终端读取请求、发送给服务器、读取服务器的响应、打印响应。

  1. 从终端读取请求
System.out.print(">");
if(!scannerConsole.hasNext()){
    break;
}
String request=scannerConsole.next();
  1. 把请求发送给服务器
writer.println(request);

对于面向字节流的传输,只需将请求写入输出流即可进行传输。但是需要注意的是,服务器端接收时是通过服务器端的输入流接收,即通过Scanner next()方法接收,而next()方法读到空白符(空格,tab,回车…)才会截止,而我们做的 请求之中本身并没有空白符,因此此处通过println来给请求手动加一个\n结尾。

  1. 从服务器读取响应
String response=scannerNetwork.next();

同理,此处需要服务器返回的响应也要以空白符结尾。

  1. 打印响应
System.out.println(response);

服务器

在编写服务器端时,要考虑到TCP传输有连接、面向字节流的特点,因此设计的框架为:

public void start() throws IOException {
   System.out.println("start!");
   while(true){
   //通过accept来“接听电话”,然后进行通信
   Socket clientSocket=serverSocket.accept();
   processConnection(clientSocket);
   }
}
//processConnection方法负责建立连接后的事务
public void processConnection(Socket clientSocket){
   try(InputStream inputStream=clientSocket.getInputStream();
       OutputStream outputStream=clientSocket.getOutputStream()){
       while(true){
       //1.读取请求
       //2.处理请求
       //3.返回响应
       //打印日志
       }
      }
}
  1. 读取请求
Scanner sc=new Scanner(inputStream);
if(!sc.hasNext()){
   System.out.printf("客户端[%s:%d]下线!\n",clientSocket.getInetAddress(),
                      clientSocket.getPort());
   break;
}
String request=sc.next();
  1. 处理请求
String response=process(request);
//编写process方法来处理请求
//对于回显服务器,只是为了观察通信过程,此处只返回请求作为响应
public String process(String request){
   return request;
}
  1. 返回响应
//outputStream.write(response.getBytes(),0,response.getBytes().length);
PrintWriter writer=new PrintWriter(outputStream);
writer.println(response);

这里有两种写法,可以直接向构建的输出流中写入响应,也可以通过printWriter来实现,这里之所以采用第二种,是为了给响应后加空白符

  1. 打印日志
System.out.printf("[%s:%d]req:%s, resp:%s\n",clientSocket.getInetAddress(),
                 clientSocket.getPort(),request,response);

三个基本问题

理论上代码写到这里也就完成了,但实际还存在三个问题:

  1. PrintWriter内置缓冲区问题
    在这里插入图片描述
    IO操作都是比较低效的操作,此处PrintWriter的内置缓冲区就是为了让这种低效操作尽可能少,简单理解就是需要填满这个缓冲区才会进行发送(合并多次IO操作为一次,提高效率)。
    但正因如此,我们每次发送的数据量比较少,无法填满这个缓冲区,那么数据就会停留在这个缓冲区里,没有被真正发送出去。
    解决方案:
writer.flush();

在每次客户端发送请求,服务器返回响应后,手动冲刷缓冲区,使数据得以发送。

  1. 服务器无法连接多个客户端
    在这里插入图片描述
    此处运行客户端1能够正常通信,而运行客户端2却无法返回响应。对于一个服务器来说,如果只能连接一个客户端,显然是不合理的。之所以造成这个问题,是因为服务器在接收到客户端1的请求后,即进入processConnection的内层循环,就无法建立新的连接。
    解决方案:
    引入多线程/线程池:
    此处展示线程池的使用:
ExecutorService pool= Executors.newCachedThreadPool();
while(true){
     //通过accept来“接听电话”,然后进行通信
     Socket clientSocket=serverSocket.accept();  
     pool.submit(new Runnable() {
          @Override
          public void run() {
              processConnection(clientSocket);
              }
    });
}

即通过主线程来接收客户端的请求,然后分配线程进行通信。
线程池相对于多线程呢,可以减少频繁创建/销毁线程的开销。
在这里插入图片描述
引入线程池后,问题可以很好的解决。
在这里插入图片描述

  1. clientSocket的关闭问题
    在服务器连接多个客户端后,就会引入多个Socket对象,而如果不手动关闭这些Socket对象,堆积的Socket会越来越多,如果不释放,就很有可能把文件描述符表占满。
    解决方案:
    每次连接断开(processConnection 结束)后,关闭其对应的clientSocket:
finally {
   try{
      clientSocket.close();
  }catch (IOException e){
       throw new RuntimeException(e);
  }
}

客户端完整代码

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;

    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 scannerConsole=new Scanner(System.in);
            Scanner scannerNetwork=new Scanner(inputStream);
            PrintWriter writer=new PrintWriter(outputStream);

            while(true){
                //1.从控制台读取请求
                System.out.print(">");
                if(!scannerConsole.hasNext()){
                    break;
                }
                String request=scannerConsole.next();

                //2.把请求发送给服务器
                writer.println(request);
                writer.flush();

                //3.从服务器读取响应
                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",10100);
        client.start();
    }
}

服务器完整代码

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;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TcpEchoServer {
    private ServerSocket serverSocket;

    public TcpEchoServer(int port) throws IOException {
        serverSocket=new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("start!");
        //线程池处理
        ExecutorService pool= Executors.newCachedThreadPool();
        while(true){
            //通过accept来“接听电话”,然后进行通信
            Socket clientSocket=serverSocket.accept();

            pool.submit(new Runnable() {
                @Override
                public void run() {
                    processConnection(clientSocket);
                }
            });
        }
    }

    public 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){
                Scanner sc=new Scanner(inputStream);
                if(!sc.hasNext()){
                    System.out.printf("客户端[%s:%d]下线!\n",clientSocket.getInetAddress(),
                            clientSocket.getPort());
                    break;
                }
                //1.读取请求,next读到空白符截止
                String request=sc.next();

                //2.处理请求
                String response=process(request);

                //3.返回响应(为了在response后加空白符)
                //outputStream.write(response.getBytes(),0,response.getBytes().length);
                PrintWriter writer=new PrintWriter(outputStream);
                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();
        }finally {
            try{
                clientSocket.close();
            }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(10100);
        server.start();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值