网络编程chapter2

1.1 创建TCP客户端

创建TCP客户端的基本步骤为:

//1、创建一个TCP客户端Socket对象,同时设置连接的服务端的ip地址和端口号
String host = InetAddress.getLocalHost();
int port = 10002;
Socket client = new Socket(host,port);
//2、获取对应的流对象,进行读取、发送数据
InputStream in = client.getInputStream();//接收服务端的数据
OutputStream out = client.getOutputStream();//向服务端发送数据
//3、断开连接,释放资源
client.close();

上面是构造一个TCP客户端的基本方法,我们来看一下各个步骤:

1.1.1 构造Socket对象

在这里插入图片描述
在这里插入图片描述
所以调用Socket的无参构造方法创建Socket对象时,我们需要利用connect()方法来和服务端对象进行连接,同时可以设置了等待连接的时间,一旦超过了这个时间,就会抛出SocketTimeOutException

在这里插入图片描述
在这里插入图片描述
创建Socket客户端时可能抛出的异常:
在这里插入图片描述

  • 抛出UnknownHostException的情况
    如果无法识别服务端的主机或者客户端的主机的ip地址,那么就会抛出UnknownHostException。
  • 抛出ConnectionException的情况
    以下2中情况会抛出ConnectionException:
    1、没有服务器进程监听指定的端口。
    2、服务器进程拒绝连接。例如我们的服务端程序为ServerSocket(int port,int backlog)构造方法的第二个参数backlog,设定服务器的连接请求队列的长度,如果队列中的连接请求已满,服务器就会拒绝其余的连接请求
  • 抛出SocketTimeoutException的情况
    当我们调用的时Socket的无参构造方法构建Socket对象的时候,那么这时候我们需要利用connect方法来设置连接的服务端的地址,同时需要设置等待连接的时间,一旦等待超时,那么就会抛出SocketTimeoutException。但是如果我们将等待的时间设置为0,表示永远不会超时
  • 抛出BindException的情况
    如果客户端Socket对象无法和当前客户端对象指定的的ip地址和端口绑定,那么就会抛出BindException。

1.1.2 获取流信息

调用getInputStream()获取读取流,从而可以读取来自服务端的信息;调用getOutputStream()可以获取写入流,可以向服务端发送数据。
但是通常我们可以利用相应的流进行修饰。

//将读取字节流转成读取字符流,然后利用BufferedRead,这样就可以调用readLine方法读取一行的数据了
BufferedReader bufR = new BufferedRead(new InputStreamReader(client.getInputStream()));

//将利用PrintWriter来修饰写入流,这样就可以调用println()方法一行一行发送数据了
PrintWriter out = new PrintWriter(client.getOutputStream(),true);//后面的true可以设置为自动刷新,这样就不需要我们发送数据之后,在调用flush方法进行刷新了

1.1.3 断开连接,释放资源

在这里插入图片描述
在这里插入图片描述

所以整个创建TCP客户端的代码格式应该是:

Socket client = null;
try{
//1、创建TCP客户端,同时设置连接的服务端的ip地址和端口
    client = new Socket(host,port);
    //2、获取对应的流
    BufferedReader bufR = new BufferedReader(new InputStreamReader(client.getInputStream()));
    PrintWriter out = new PrintWriter(client.getOutputStream(),true);
    //3、和服务端进行数据的交互
}catch(IOException e){
    e.printStackTrace();
}finally{
//4、保证socket对象能够被关闭,那么将其放在finally中
    try{
       if(socket != null){
           socket.close();
       }
    }catch(IOException e){
       e.printStackTrace();
    }
}

1.2 获取Socket对象的相关信息

在这里插入图片描述

1.3 半关闭的Socket

如果进程A和进程B进行Socket通信,假定进程A输出数据,进程B读入数据。进程A如何告诉进程B所有的数据已经全部输出呢?主要由以下几种方法:

  • 彼此约定一个结束标志,当进程A输出了这个结束表示,那么就将其发送给进程B,然后进程A结束,当进程B读取到这个结束标志之后,就可以知道进程A输出完毕了,不需要再进行读取的操作了

  • 进程A先发送一个信息,告诉进程B所发送的正文的长度,然后再发送正文。进程B先获取到了进程A发送的正文的长度,接下来只要读取完该长度的字符或者字节,就停止读取数据。

  • 调用shutdownOutput()或者shutdownInput()方法,提醒对方,停止了发送数据或者停止读取数据。所以当进程A输出完毕之后,调用shutdownInput方法,那么进程B就可以知道已经读取完毕了

  • 当进程A发送所有的数据之后,调用close方法关闭进程A,那么当进程B读取了进程A发送的所有数据之后,再次读取的时候,例如调用的时read()返回的是-1,或者调用readLine()方法进行读取的时候返回的是null,就是因为进程A关闭了,此时的进程B不可以再读取了。
    在这里插入图片描述
    ⭐⭐⭐当客户端和服务端进行通信的时候,如果有一方突然结束程序,或者关闭了Socket,或者单独关闭了输入流或输出流,对另一方会造成什么影响?

  • 突然有一方结束程序
    ⭐⭐⭐如果我们突然终止了TCP客户端(即客户端没有来得及调用close()方法,客户端程序结束了),那么就会抛出错误:Exception in thread "main" java.net.SocketException: Connection reset

TCP的客户端:

public class ClientDemo2 {
    public static void main(String[] args) throws IOException {
        //创建TCP客户端,同时设置连接的服务端的ip地址和端口
        Socket client = new Socket(InetAddress.getLocalHost(), 10002);
        //2、获取对应的流,进行读取数据
        BufferedReader in =
                new BufferedReader(new InputStreamReader(System.in));//读取控制台输入的数据
        PrintWriter out = new PrintWriter(client.getOutputStream(), true);//true用于自动刷新
        String line;
        while((line = in.readLine()) != null){
            //向服务端发送数据,当输入over的时候关闭客户端
            out.println(line);
            if(line.equals("over")){
                break;
            }
        }
        //3、断开连接,释放资源
       // client.close();
    }
}

TCP服务端:

public class ServerDemo2 {
    public static void main(String[] args) throws IOException {
        //1、新建TCP服务端,同时设置对应的端口
        ServerSocket server = new ServerSocket(10002);
        //2、由于server没有获取对应的流方法,所以需要调用accept方法来获取连接的客户端
        Socket client = server.accept();
        //3、client对象调用对应的方法获取对应的流
        BufferedReader r
                = new BufferedReader(new InputStreamReader(client.getInputStream()));
        String line;
        while((line = r.readLine()) != null){
            System.out.println(line);
        }
        //4、断开连接,释放资源
        client.close();
        server.close();
    }
}

上面的代码中客户端还没有调用close方法就关闭程序了,那么这时候就会导致服务端再收到了客户端发送的over字样之后,将其打印到控制台中,然后就会发生了报错,提示Exception in thread "main" java.net.SocketException: Connection reset

所以我们将close()方法放在finally中,可以在一定的程度避免发生这样的错误。

⭐⭐⭐但是如果我们突然结束了服务端的程序,即没有调用close()方法关闭服务端,那么客户端并不会抛出错误,此时上面的代码依旧需要我们输入over字样的时候,客户端才会结束程序(这个可以我们再运行的时候,自己点击对应的地方来结束服务端的程序,之后发现客户端还在处于运行的状态,之后输入over,那么客户端就会结束程序了,证明结论是正确的)
在这里插入图片描述

  • 关闭了Socket
    如果我们关闭了Socket对象,那么服务端再次读取数据的时候,就会默认为读取不到数据了,那么这时候如果我们利用的是InputStream调用的read方法,那么返回的是-1,如果利用的是BufferedRead的readLine()方法,那么返回的是null,此时服务端就会结束读取数据了
  • 单独关闭了输入流或者输出流
    如果我们客户端单独关闭了输入流或者输出流,那么这时候只是提醒服务端,客户端已经停止读取数据了或者停止发送数据(即将所有的数据都发送出去了),那么这时候执行这一步之后,服务端就知道已经读取了全部数据了,那么不会再进行读取的操作

1.3 创建ServerSocket服务端

再了解上面TCP的客户端Socket对象的相关操作之后,那么创建ServerSocket服务端的用法是相似的:

//1、创建TCP服务端ServerSocket,同时需要设置对应的端口,需要和上面客户端设置的服务端的端口要一致
int port = 10002;
ServerSocket server = new ServerSocket(port);
//2、由于ServerSocket对象没有获取流的方法,所以需要利用accept方法来获取和当前服务端建立连接的客户端Socket对象
Socket client = server.accept();
//3、Socket对象调用相关的方法获取流对象,然后和客户端进行数据的交互
BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
PrintWriter out = new PrintWriter(client.getOutputStream(),true);//true用于自动刷新
//4、交互完毕之后,断开连接,释放资源
client.close();
server.close();

如果我们需要实现当前的服务端需要和多个客户端创建连接,那么我们可以保证这个服务端不要关闭,一直都在运行的状态:

while(true){
//保证了服务端一直再运行状态
   Socket client = null;
   try{
       //获取和服务端连接的客户端
       client = server.accept();
       //获取对应的流对象,进行数据交互
   }catch(IOException e){
       e.printStackTrace();
   }finally{
   /*
   将和服务端建立连接的客户端从连接请求队列中移除,这样就可以避免了服务端的连接请求队列满了,然
   后又会有其他的客户端B希望和当前的服务端建立连接,此时由于服务端连接请求队列已经满了,那么就会
   拒绝客户端B的连接请求。然后客户端B的socket对象因为这个,抛出了异常ConnectionExcpetion。
   所以需要保证当前和服务端建立连接的客户端将所有的数据都交互完毕之后,需要将其从服务端的连接请
   求队列中移除,所以将关闭client的代码放在finally中执行。
   */
       try{
         if(client != null){
            client.close();
         }
       }catch(IOException e){
         e.printStackTrace();
       }
   }
}

其中创建ServerSocket对象有几种重载方式:
在这里插入图片描述
在这里插入图片描述
如果利用的是ServerSocket的无参构造方法进行构造ServerSocket对象的时候,那么这时候的ServerSocket对象并没有和任何端口进行绑定,需要我们执行bind()方法绑定端口通过这个无参构造方法,那么就可以允许服务器再绑定到指定的端口之前,先设置ServerSocket的一些选项。因为一旦服务器与特定的端口绑定之后,有一些选项不能再改变了,此时再取设置某一些选项,例如调用setReuseAddress(true)的时候不会在生效
在这里插入图片描述

同时,在创建ServerSocket对象的时候,我们也可以设置这个服务端的连接请求队列backlog的大小,因为这个的存在,那么服务器可以同时监听到多个客户端的连接请求
在这里插入图片描述
在这里插入图片描述

1.4 创建多线程的服务器

尽管服务器可以和多个客户端建立连接,但是服务端只能处理完一个客户端之后,才可以处理下一格客户端的请求。也就是说,多个客户必须要排队等候服务端的相应,只有处理完一个客户端之后,才可以处理下一个,不可以同时处理多个客户

在这里插入图片描述
所以我们给每一个客户端分配一个线程的服务端代码应该是这样的:

public class ThreadServerDemo {
    private ServerSocket server;
    public ThreadServerDemo(int port) throws IOException {
        server = new ServerSocket(port);
    }
    public static void main(String[] args) throws IOException {
        //1、创建一个端口号为10002的服务端
        ThreadServerDemo test = new ThreadServerDemo(10002);
        test.service();
    }

    private void service() {
        /*
        之所以没有将client定义再try里面,是因为如果定义再try中,那么
        是一个再try范围中的局部变量,此时finally中没有client这个变量。
        因此要保证client能够从连接请求队列中移除,就需要保证client被关闭,
        所以一定需要执行finally中的代码,所以将client放在try的外面
         */
        Socket client = null;
        while(true){
            //Socket client = null;
            try {
                //2、获取和当前服务端建立连接的客户端对象
                client = server.accept();
                System.out.println("这是端口号为 " + client.getPort() + " 的客户和服务端建立了连接!");
                /*
                3、由于如果是单线程,那么这时候的服务端虽然能同时和多个
                客户端建立连接,但是只能给一个客户端进行数据交互,当当前
                的客户端处理完毕,关闭这个客户端,才可以和下一个客户端进行
                数据交互。
                因此我们利用多线程,来实现这个线程实现服务端同时和多个客户
                进行数据交互
                 */
                Thread thread = new Thread(new SocketRunnable(client));
                //线程对象调用start方法开启线程,执行线程任务
                thread.start();
            } catch (IOException e) {
                e.printStackTrace();
            }
            /*
            这里没有写finally,是因为再Runnable的子类中,已经执行了close方法了,否则
            如果再这里写finally的话,那么当执行完thread.start()的时候,一旦执行了finally中
            关闭client的操作,那么就会发生报错,提示Socket is closed的错误
             */
        }
    }
}

实现Runnable接口的子类为SocketRunnable:

public class SocketRunnable implements Runnable {
    private Socket client;
    public SocketRunnable(Socket client){
        this.client = client;
    }

    @Override
    public void run() {
        try{
            //2、获取流对象
            BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
            PrintWriter out = new PrintWriter(client.getOutputStream(), true);
            String line;
            //3、和客户端进行通信
            while((line = in.readLine()) != null){
                System.out.println(line);
                //将当前客户端发来的字符串转成大写,之后发送给客户端
                out.println(line.toUpperCase());
            }
        }catch(IOException e){
            e.printStackTrace();
        }finally{
            try{
                if(client != null){
                    client.close();
                }
            }catch(IOException e){
                e.printStackTrace();;
            }
        }
    }
}

但是为每一个客户端创建一个线程会有弊端:
在这里插入图片描述
线程池解决方案看的不是很懂,所以就暂时不写了,大家可以自行去学习,我弄懂了之后再去更新哈!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值