【网络编程】学习系列(二)基于TCP的Socket通信

基于TCP的Socket通信

Tcp提供了基于流的长连接的数据传递,发送的数据带有顺序性。TCP是一种流协议,以六为单位进行数据传输。

什么是长连接?

长连接可以实现客户端与服务端连接成功后连续的传输数据,在这个过程中,连接保持开启状态。数据传输完后连接不关闭。

总之:长连接是指建立了Socket连接后,无论是否使用这个连接,该链接都保持连接状态。

什么是短连接?

短连接就是服务端与客户端连接成功后开始传输数据,数据传输完毕则连接立即关闭,如果还想传输数据,则需要再创建新的连接进行数据传输。

UDP是无连接协议,所以不存在长短连接的概念

长连接的优缺点

  • 缺点:除第一次之外,客户端不需要每次传输数据时都先与服务端进行握手,这样就减少了握手确认时间,直接传输数据,提高程序运行效率
  • 优点:在服务端需要保存多个Socket对象,增加内存占用

短连接的优缺点

  • 缺点:每次传输数据前都要重新创建连接,也就是每次都要进行三次握手。
  • 优点:在服务端不需要保存多个Socket对象,降低内存占用

1.1 ServerSocket类的accept()方法

ServerSocket的作用是创建Socket(套接字)的服务端,而Socket类的作用是创建Socket的客户端。在代码上就是使用Socket类去连接ServerSocket类,也就是客户端要主动连接服务端。

ServerSocket类中的 public Socket accept()方法的作用是真挺并接受此套接字的连接。此方法在连接传入之前一直阻塞。

ServerSocket accept()在没有客户端连接时是阻塞的

测试代码如下

如下代码创建了端口号为8088的服务端,然后等待客户端连接,没有客户端连接的话一直阻塞

@Test
public void test13() throws IOException {
    System.out.println("服务器启动8088");
    ServerSocket serverSocket = new ServerSocket(8088);
    Socket accept = serverSocket.accept();
    System.out.println("客户端连接成功 -  ");
    serverSocket.close();
}

创建客户端,先运行服务端,然后运行客户端,就会发现服务端不会阻塞了。

@Test
public void test14() throws IOException {
    System.out.println("客户端启动");
    //连接到本地的8088端口
    Socket socket = new Socket("localhost",8088);
    System.out.println("客户端结束 - > ");
    socket.close();
}

使用ServerSocket创建一个Web服务器

@Test
public void test15(){
    ServerSocket serverSocket = null;
    Socket client = null;
    InputStream inputStream = null;
    OutputStream outputStream = null;
    BufferedReader bufferedReader = null;
    try {
        serverSocket = new ServerSocket(8888);
        client = serverSocket.accept();
        //使用客户端写点数据出去
        outputStream = client.getOutputStream();
        inputStream = client.getInputStream();
        //用转换流将其转换
        InputStreamReader isr = new InputStreamReader(inputStream);
        OutputStreamWriter osw = new OutputStreamWriter(outputStream);
        //交给Buffer处理速度更快
        bufferedReader = new BufferedReader(isr);
        //将客户端的数据读取出来--打印--一行一行读取
        String res = "";
        while(!"".equals(res = bufferedReader.readLine())){
            System.out.println(res);
        }
        //向客户端响应点数据进行测试
        outputStream.write("HTTP/1.1 200 OK\r\n\r\n".getBytes());
        outputStream.write("<html><body> <a href='www.baidu.com'>test</a> </body></html>".getBytes("UTF-8"));
    }catch (IOException e){
        e.printStackTrace();
    }finally {
        try {
            if(outputStream != null){
                outputStream.flush();
                outputStream.close();
            }
            if(inputStream != null){
                //外层被关闭,内层的InputStream也会关闭
                bufferedReader.close();
            }
            client.close();
            serverSocket.close();
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

1.2 Socket中的InputStream的read()方法

注意:此方法和accept()方法一样都具有阻塞特性

此方法的作用是从流中对方发来的数据。

测试程序

如下程序中启动服务器,然后启动客户端,会发现服务器一直阻塞,因为read()没读到任何数据,因为客户端没法送任何数据,当时在10s后就行了,因为客户端退出了

服务端如下所示

@Test
public void test16(){
    try {
        byte[] bytes = new byte[1024];
        System.out.println("--------- 服务器启动 --------");
        ServerSocket serverSocket = new ServerSocket(8888);
        Socket socket = serverSocket.accept();
        InputStream inputStream = socket.getInputStream();
        System.out.println("----------开始读取数据---------");
        inputStream.read(bytes);
        System.out.println("----------读取完成---------");
        serverSocket.close();
    }catch (IOException e){
        e.printStackTrace();
    }
}

客户端如下

@Test
public void test17(){
    try {
        System.out.println("客户端开始连接");
        Socket socket = new Socket("127.0.0.1", 8888);
        TimeUnit.SECONDS.sleep(10L);
        System.out.println("客户端结束------");
        socket.close();
    }catch (IOException | InterruptedException e){
        e.printStackTrace();
    }
}

1.3 客户端向服务端传递字符串

代码分析

服务器端开设端口,等待客户端连接,

客户端连接成功后开始发送数据。

服务器端接受数据,但是每次只接受3个字符,并打印

服务器端如下所示

@Test
public void test18(){
    ServerSocket serverSocket = null;
    InputStream inputStream = null;
    InputStreamReader inputStreamReader = null;
    try {
        char[] chars = new char[3];
        serverSocket = new ServerSocket(8888);
        Socket client = serverSocket.accept();
        inputStream = client.getInputStream();
        inputStreamReader = new InputStreamReader(inputStream);
        int readLength = 0;
        System.out.println("---------read start----------");
        while((readLength = inputStreamReader.read(chars)) != -1){
            String s = new String(chars, 0, readLength);
            System.out.println(s);
        }
        System.out.println("---------read end----------");
    }catch (IOException e){
        e.printStackTrace();
    }finally {
        try {
            if(inputStreamReader != null){
                inputStreamReader.close();
            }
            if(serverSocket != null){
                serverSocket.close();
            }
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

客户端如下所示

@Test
public void test19(){
    Socket socket = null;
    OutputStream outputStream = null;
    BufferedWriter bw = null;
    try {
        socket = new Socket("127.0.0.1",8888);
        outputStream = socket.getOutputStream();
        bw = new BufferedWriter(new OutputStreamWriter(outputStream));
        System.out.println("客户端写入数据开始");
        TimeUnit.SECONDS.sleep(3L);
        bw.write("我是东坡");
        System.out.println("客户端写入结束");
    }catch (IOException | InterruptedException e){
        e.printStackTrace();
    }finally {
        try {
            if(bw != null){
                bw.flush();
                bw.close();
            }
            if(socket != null){
                socket.close();
            }
        }catch (IOException e){
            e.printStackTrace();
        }

    }
}

1.4 服务端向客户端传递数据

服务器端如下

@Test
public void test20(){
    ServerSocket serverSocket = null;
    BufferedWriter bw = null;
    try {
        serverSocket = new ServerSocket(8888);
        Socket client = serverSocket.accept();
        bw = new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));
        bw.write("我是东坡,这是测试");
    }catch (IOException e){
        e.printStackTrace();
    }finally {
        try {
            if(bw != null){
                bw.flush();
                bw.close();
            }
            if(serverSocket != null){
                serverSocket.close();
            }
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

客户端如下

@Test
public void test21(){
    Socket socket = null;
    BufferedReader br = null;
    try {
        socket = new Socket("localhost",8888);
        br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        System.out.println("客户端开始 -------");
        String s = br.readLine();
        System.out.println("客户端接收数据为 : "+ s);
        System.out.println("客户端结束 -------");
    }catch (IOException e){
        e.printStackTrace();
    }finally {
        try {
            if(br != null){
                br.close();
            }
            if(socket != null){
                socket.close();
            }
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

1.5 实现服务端与客户端多次的往来通信

前面的实验都是服务端与客户端只进行了一次通信,那么如何实现多次的长连接通信呢?

代码分析

使用对象流ObjectOutputStream

服务端代码如下所示

@Test
public void test22(){
    ServerSocket serverSocket = null;
    ObjectInputStream ois = null;
    ObjectOutputStream oos = null;
    String msgA = "你好客户端A - 1\r\n";
    String msgB = "你好客户端B - 1\r\n";
    try {
        serverSocket = new ServerSocket(8888);
        Socket client = serverSocket.accept();
        ois = new ObjectInputStream(client.getInputStream());
        oos = new ObjectOutputStream(client.getOutputStream());
        //先把长度发送过去,对面根据这个长度来创建数组
        oos.writeInt((msgA + msgB).getBytes().length);
        oos.write(msgA.getBytes());
        oos.write(msgB.getBytes());
        oos.flush();
        //读取数据
        int legth = ois.readInt();
        byte[] bytes = new byte[legth];
        ois.readFully(bytes);
        System.out.println(new String(bytes));
    }catch (IOException e){
        e.printStackTrace();
    }finally {
        try {
            if(oos != null){
                oos.close();
            }
            if(ois!= null){
                ois.close();
            }
            if(serverSocket != null){
                serverSocket.close();
            }

        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

客户端代码如下所示

@Test
public void test23(){
    Socket client = null;
    ObjectOutputStream oos = null;
    ObjectInputStream ois = null;
    String msgA = "你好服务端A\r\n";
    String msgB = "你好服务端B\r\n";
    try {
        client = new Socket("localhost",8888);
        oos = new ObjectOutputStream(client.getOutputStream());
        ois = new ObjectInputStream(client.getInputStream());
        oos.writeInt((msgA + msgB).getBytes().length);
        oos.write(msgA.getBytes());
        oos.write(msgB.getBytes());
        oos.flush();
        int length = ois.readInt();
        byte[] bytes = new byte[length];
        ois.readFully(bytes);
        System.out.println(new String(bytes));
    }catch (IOException e){
        e.printStackTrace();
    }finally {
        try {
            if(oos != null){
                oos.close();
            }
            if(ois!= null){
                ois.close();
            }
            if(client != null){
                client.close();
            }
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

1.6 Stream的close()方法造成Socket关闭

如下代码运行会出现SocketException socket is closed

原因如下所示:在关闭获取的流的时候,默认会关闭当前的socket,此时socket已经关闭,再次获取他的输出流就会抛出异常。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wtESjquo-1603961482653)(images/异常.png)]

socket获取的输入流为SocketInputStream,其close方法如下
在这里插入图片描述
服务端如下所示

@Test
public void test1(){
    try {
        int len = 0;
        byte[] bytes = new byte[20];
        ServerSocket serverSocket = new ServerSocket(8888);
        System.out.println("服务器启动");
        Socket client = serverSocket.accept();
        InputStream inputStream = client.getInputStream();

        while((len = inputStream.read(bytes)) != -1){
            System.out.println(new String(bytes,0,len));
        }
        inputStream.close();
        OutputStream outputStream = client.getOutputStream();
        client.close();
        serverSocket.close();
        System.out.println("服务器终止");
    }catch (IOException e) {
        e.printStackTrace();
    }
}

客户端代码

@Test
public void test2(){
    try {
        Socket socket = new Socket("localhost", 8888);
        System.out.println("客户端启动");
        OutputStream outputStream = socket.getOutputStream();
        outputStream.write("我是中国人".getBytes());
        outputStream.close();
        TimeUnit.SECONDS.sleep(100);
    }catch (IOException | InterruptedException e){
        e.printStackTrace();
    }finally {

    }
}

1.7 客户端传输图片到服务器

服务端代码

@Test
public void test3(){
    ServerSocket serverSocket = null;
    FileOutputStream fos = null;
    try {
        byte[] bytes = new byte[1024];
        int len = 0;
        serverSocket = new ServerSocket(8888);
        System.out.println("服务器启动------");
        Socket client = serverSocket.accept();
        InputStream inputStream = client.getInputStream();
        fos = new FileOutputStream("D:\\aa.jpg");
        while((len = inputStream.read(bytes)) != -1){
            fos.write(bytes,0,len);
        }
        fos.flush();
        System.out.println("服务器保存文件数据成功------");
    }catch (IOException e){
        e.printStackTrace();
    }finally {
        //关闭流
        try {
            if(fos != null){
                fos.close();
            }
            if(serverSocket != null){
                serverSocket.close();
            }
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

客户端代码

@Test
public void test4(){
    Socket socket = null;
    FileInputStream fis = null;
    try {
        byte[] bytes = new byte[1024];
        int len = 0;
        socket = new Socket("localhost", 8888);
        System.out.println("连接服务器成功------");
        fis = new FileInputStream("C:\\Users\\dongpo\\Pictures\\Saved Pictures\\1.jpg");
        OutputStream outputStream = socket.getOutputStream();
        while((len = fis.read(bytes)) != -1){
            outputStream.write(bytes,0,len);
        }
        outputStream.flush();
        System.out.println("客户端发送图片成功------");
    }catch (IOException e){
        e.printStackTrace();
    }finally {
        try {
            if(fis != null){
                fis.close();
            }
            if(socket != null){
                socket.close();
            }
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

1.8 TCP三次握手连接过程

什么是TCP的三次握手,如下图所示

各个参数的含义

  • seq:发送数据的序列号
  • ack:对于seq的确认,假设现在发送了100个字节的数据,如何确认?当ack=101时就代表前100个都收到了,因为101是他期待收到的数据。
  • SYN:同步标志位:表示要请求连接
  • ACK:确认标志位

三次握手的分析如下

第一次握手:

为什么seq=100?这里只是一个随机值,本来sql在设计的时候就是一个随机值。SYN=1?请求发起连接当然为1

第二次握手

为什么ack=101。服务现在想要接受第101个数据,说明我前面的都收到了。这是对客户端发送请求数据的一个确认,

ACK=1是确认标志位有效

第三次握手

为什么进行第三次握手:在第一次握手,服务器收到了这个连接请求并且给出确认。但是服务器并不知道我的确认客户端收没收到

如果只有两次握手:第二次的握手客户端没有收到,就会变成我不知道第一次发送的字节他收没收到,我下一次到底是从哪个序列开始发送数据:还是会从100开始发送数据

还有一种解释:当客户端发送第一个握手时,网络延时很长时间才到达了服务器端,而在中间时间超时了客户端的连接断开。之后服务器收到了客户端的请求误以为是请求连接的,然后并给出响应和确认,但是客户端已经断开了。如果只有两次握手此时服务器就会傻傻的等待客户端发送数据,殊不知客户端已经断开。

当有了三次握手之后:服务给到了客户端确认,当客户端收到了消息,再给服务器确认这样就避免了上面的问题。

在这里插入图片描述

1.9 服务端与客户端互传对象以及I/O流顺序问题

服务器代码

@Test
public void test7(){
    try {
        ServerSocket serverSocket = new ServerSocket(8888);
        Socket client = serverSocket.accept();
        System.out.println("客户端连接---"+client.getPort());
        InputStream inputStream = client.getInputStream();
        OutputStream outputStream = client.getOutputStream();
        ObjectInputStream ois = new ObjectInputStream(inputStream);
        ObjectOutputStream oos = new ObjectOutputStream(outputStream);
        System.out.println("开始发送数据");
        for(int i = 0; i<5;i++){
            User o = (User)ois.readObject();
            System.out.println("服务器 => "+i+"读取的对象"+o);
            User newUser = new User(i, "server" + i);
            oos.writeObject(newUser);
        }
        oos.close();
        ois.close();
        outputStream.close();
        inputStream.close();
        client.close();
        serverSocket.close();
    }catch (IOException | ClassNotFoundException e){
        System.out.println("服务器异常");
    }
}

客户端如下所示

@Test
public void test8(){
    try {
        Socket socket = new Socket("localhost", 8888);
        InputStream inputStream = socket.getInputStream();
        OutputStream outputStream = socket.getOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(outputStream);
        ObjectInputStream ois = new ObjectInputStream(inputStream);
        for(int i = 0;i<5;i++){
            oos.writeObject(new User(i,"client"));
            //读取服务器发来的对象
            User user = (User)ois.readObject();
            System.out.println("client => "+i+user);
        }
        oos.close();
        ois.close();
        outputStream.close();
        inputStream.close();
        socket.close();
    }catch (IOException | ClassNotFoundException e){
        System.out.println("客户端异常");
    }
}

此时如果将客户端获取输入流和输出流的代码调换位置,就会发生阻塞的情况

因为服务器端先获取输入流,在获取输出流

客户端也是先获取输入流在获取输出流,只要改变两个任意一个顺序就行
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值