[线程与网络] 网络编程与通信原理(三):网络编程Socket套接字(TCP协议)

🌸个人主页:https://blog.csdn.net/2301_80050796?spm=1000.2115.3001.5343
🏵️热门专栏:🍕 Collection与数据结构 (92平均质量分)https://blog.csdn.net/2301_80050796/category_12621348.html?spm=1001.2014.3001.5482
🧀Java EE(94平均质量分) https://blog.csdn.net/2301_80050796/category_12643370.html?spm=1001.2014.3001.5482
🍭MySql数据库(93平均质量分)https://blog.csdn.net/2301_80050796/category_12629890.html?spm=1001.2014.3001.5482
🍬算法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12676091.html?spm=1001.2014.3001.5482
感谢点赞与关注~~~
在这里插入图片描述

1. TCP套接字编程

前面的UDP套接字是基于数据报为基本单位进行传输的,而今天我们要叙述的TCP套接字是基于字节为单位进行传输的.

1.1 API介绍

1.1.1 ServerSocket

本类是创建在服务器端的Socket的API.

  • 构造方法:
方法签名方法说明
ServerSocket(int port)创建⼀个服务端流套接字Socket,并绑定到指定端⼝
  • 常用成员方法
方法签名方法说明
Socket accept()开始接听指定端⼝(创建时绑定的端⼝),有客户端连接后,返回⼀个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待(没有客户端与之进行连接),注意这里返回的数据类型是Socket类型
void close()关闭此套接字

举例说明:买房
有一天一位老哥想要买一套房子,他在马马路牙子上见到了一位西装革履的销售小哥,这时候这位销售小哥就问老哥:“哥,要买房吗?”.这位老哥正好想买套房子,于是跟着小哥去了售楼部.到了售楼部之后,这位小哥一挥手,来了一个Leader风格穿着的小姐姐,哥老哥一桶踢里哐啷地介绍房子.
在这个过程中,ServerSocket的构造方法的作用就好像是这个销售小哥一样,用来揽客.(用来和客户端用端口号建立连接),而这个售楼部的小姐姐就好像ServerSocket中的accept方法一样,在销售小哥揽客之后,接待客户(在与客户端建立连接之后,接通客户端的请求).
在这里插入图片描述

1.1.2 Socket

Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服
务端Socket
,也就是Socket类在客户端和服务器中都要用.
不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据
的。

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

[注意] 客户端与服务器进行连接的时候,构造方法本身就可以与指定的服务器进行连接,也就是拨号的过程.

  • Socket的常用成员方法
方法签名方法说明
InetAddress getInetAddress()返回套接字所连接的地址
InputStream getInputStream()返回此套接字的输⼊流
OutputStream getOutputStream()返回此套接字的输出流

这里我们看到,在使用这些成员方法的时候,可以获取到Socket套接字的输入流和输出流,之后我们在客户端这边向着服务器发送请求的时候,使用的是输出流,反之从服务器获取响应的时候,我们用的是输入流,反之,在服务器这边,向着客户端返回响应的时候,使用输出流,接收请求的时候,使用输入流,这里的输入和输出的参照是以自身为参照.后续我们使用read()或者Scanner从InputStream读取数据之后,就可以对数据进行操作,处理好之后,在通过write()操作写回OutputStream.

[注意] 这里使用到的输入流和输出流,我们在之前的文件IO章节提到过.
https://blog.csdn.net/2301_80050796/article/details/138851572?spm=1001.2014.3001.5501

1.2 代码实例

在代码中,我们整体会涉及到三个Socket套接字:

  1. 服务器ServerSocket
  2. 服务器Socket,通过这个Socket和客户端提供交换能力
  3. 客户端Socket,通过这个Socket和服务器提供交换能力

1.2.1 回显服务器

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

public class TCP_Echo_Server {
    private ServerSocket server = null;//服务器用于操作网卡的Socket
    //构造方法指定端口号
    public TCP_Echo_Server(int port) throws IOException {
        server = new ServerSocket(port);//传入端口号,创建服务器Socket
    }
    public void start() throws IOException {
        System.out.println("服务器启动");
        while (true){
            Socket clientSocket = server.accept();//接收客户端请求,返回Socket对象
            processConnection(clientSocket);//传入accept的请求,处理请求
        }
    }
    private void processConnection(Socket cilentSocket) throws IOException {
        //打印登录日志
        System.out.printf("[%s:%d] 客户端上线\n",cilentSocket.getInetAddress(),cilentSocket.getPort());
        //从获取到的请求中获取到输入流和输出流,由于它们两个需要关闭,所以写在try中
        try (InputStream inputStream = cilentSocket.getInputStream();
        OutputStream outputStream = cilentSocket.getOutputStream()){
            while (true){
                Scanner scanner = new Scanner(inputStream);//使用Scanner的方式读取输入流中的数据
                if (!scanner.hasNext()){//如果输入流中没有数据了,就结束循环
                    System.out.printf("[%s:%d] 客户端下线\n",cilentSocket.getInetAddress(),cilentSocket.getPort());
                    break;
                }
                String request = scanner.next();//读取字符串
                String response = process(request);//处理请求,返回响应
                outputStream.write(response.getBytes());//把响应写回输出流
                System.out.printf("[%s:%d] req = %s,resp = %s\n",cilentSocket.getInetAddress(),cilentSocket.getPort(),
                request,response);//打印请求响应日志
            }
        }finally {
            cilentSocket.close();//关闭Socket.每一个客户端请求都有一个Socket,用完就没用了,所以要关闭
        }
    }
    private String process(String request){
        return request + '\n';//之所以要加上一个换行符,是因为响应写回到客户端要把一大串字节分为若干数据报
    }

    public static void main(String[] args) throws IOException {
        TCP_Echo_Server tcpEchoServer = new TCP_Echo_Server(9090);
        tcpEchoServer.start();
    }
}

[注意的几点]

  1. 注意在accept接听请求的时候,在外层加上while死循环.
  2. 服务器在通过ServerSocket接听客户端发送来的请求并读取数据之后,需要关闭cilentSocket对象,而且为了防止程序在处理请求的过程中出现异常导致程序以外终止,我们还需要把它放入finally中.不关闭就会导致文件资源泄露,是当前的cilentSocket对象的文件描述符表得不到释放.

我们还是那上面的买房的例子来说明
销售小哥不可能只揽你一个人,他需要不停地从外面揽客,就需要不停有小姐姐来接待,所以需要while死循环.而售楼部的小姐姐也不可能光给你自己提供服务,而是在接待完你之后,还会在接待其他人,所以在接待你之后,就需要close.

  1. 就像我们前面说的,使用InputStream和OutputStream之后需要关闭,所以我们就需要把输入流和输出流的创建写入try的括号中.
  2. 这里不使用read()读取,读取出来的是字节byte[],使用Scanner进行读取的时候,会对字节进行转换,自动转换为字符串.
  3. scanner.next()读取数据的时候,由于我们后边写到的客户端会使用"空白符"来将一大串数据进行分割,每次分割都对应的是一个应用层数据报,所以我们这里在使用next读取的时候,读取到的就是一个个完整的"字节流请求",而不是没有分隔符的"一大坨数据".
  4. 与第5条相同的道理,在返回响应的时候,我们同样要在数据的末尾加上"空白符",以便客户端进行读取. return request + '\n';

1.2.2 回显客户端

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;

public class TCP_Echo_Client {
    private Socket socket = null;//客户端用于操作网卡的Socket

    public TCP_Echo_Client(String IP,int Port) throws IOException {
        socket = new Socket(IP,Port);//通过构造方法传入的服务器的IP地址和端口号构造Socket
    }
    public void start() throws IOException {
        System.out.println("客户端启动");
        Scanner scanner = new Scanner(System.in);//客户端通过控制台输入请求
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()){
            while (true){
                System.out.println(">>");
                String request = scanner.next();//通过Scanner获取到要发送的请求
                request += '\n';//为请求加上一个空白符,以便服务器通过Scanner.next读取
                outputStream.write(request.getBytes());//写入输出流
                Scanner scanner1 = new Scanner(inputStream);//从输入流读取服务器响应
                String response = scanner1.next();
                System.out.println(response);
            }
        }
    }

    public static void main(String[] args) throws IOException {
        TCP_Echo_Client tcpEchoClient = new TCP_Echo_Client("127.0.0.1",9090);
        tcpEchoClient.start();
    }
}

[需要注意的几点]

  1. request += '\n';这里要在请求的后面加上\n,这样在服务器读取响应的时候,就是一个个完整的"字节流请求".
  2. scanner1.next();读取"字节流响应"的时候,由于前面服务器使用\n对数据进行过分割,所以我们在这里就可以使用这样的方式进行读取.

1.3 为服务器引入多线程

上面的代码出现的最大的问题就是,不可以同时给多个客户端提供服务.这其实不是API的问题,是我们代码结构的问题,就是由于我们在上述的代码中出现了死循环嵌套的代码方式:
第一个客户端在被服务器accept()接听之后,进入processConnection(clientSocket);方法内部会有while (true)循环,这就使得服务器在没有等到第一个客户端结束运行之后,上层start()方法中的死循环无法进行下一个循环接听下一个客户端.所以会出现不支持同时给多个客户端提供服务的现象.
在这里插入图片描述
先启动的客户端有响应.
在这里插入图片描述
后启动的客户端无响应.
所以我们在服务器的代码中引入了多线程.accept()接听一个客户端,就给这个客户端的Socket单独创建一个线程,这样在接听下一个客户端的时候,就不会受上一个客户端的影响.

  • 引入多线程
public void start() throws IOException {
        System.out.println("服务器启动");
        while (true){
            Socket clientSocket = server.accept();//接收客户端请求,返回Socket对象
            Thread thread = new Thread(()->{//使用多线程同时处理多个客户端发来的请求
                try {
                    processConnection(clientSocket);//传入accept的请求,处理请求
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
            thread.start();
        }
    }

其中每个创建的新线程为每个客户端的Socket提供服务,而main线程用来循环处理接听操作.
当然我们也可以引入线程池,我们在前面提到过线程池的概念.
https://blog.csdn.net/2301_80050796/article/details/138388735?spm=1001.2014.3001.5501
从下图可以看到,同时启动的两个客户端都有反应:
在这里插入图片描述
在这里插入图片描述

服务器也有两个客户端的登录日志:
在这里插入图片描述

  • 引入线程池
 public void start() throws IOException {
        System.out.println("服务器启动");
        ExecutorService executorService = Executors.newCachedThreadPool();//使用线程池同时处理多个客户端发来的请求
        while (true){
            Socket clientSocket = server.accept();//接收客户端请求,返回Socket对象
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        processConnection(clientSocket);//传入accept的请求,处理请求
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
        }
    }

[注意]

  • 这里我们使用线程数量动态变化的线程池创建方式.Executors.newCachedThreadPool();,而使用创建固定线程的线程池创建方式不合乎逻辑,我们不可能只给固定几个客户端提供服务.
  • 这里我们使用线程池的方式,比循环创建线程的方式更加高效,创建线程会涉及到线程的创建和销毁,会有一定的开销,而线程池在用完一个线程之后,就会将这个线程回收回线程池,使得每一个线程更加充分地利用起来.

2. 拓展了解:长连接与短连接

TCP发送数据时,需要先建⽴连接,什么时候关闭连接就决定是短连接还是⻓连接:
短连接:每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能⼀次收发
数据。
长连接:不关闭连接,⼀直保持连接状态,双方不停的收发数据,即是长连接。也就是说,长连接可
以多次收发数据。

基于BIO(同步阻塞IO)的长连接会⼀直占⽤系统资源。对于并发要求很⾼的服务端系统来说,这样的消耗是不能承受的。
由于每个连接都需要不停的阻塞等待接收数据,所以每个连接都会在⼀个线程中运⾏
⼀次阻塞等待对应着⼀次请求、响应,不停处理也就是⻓连接的特性:⼀直不关闭连接,不停的处理
请求。
实际应⽤时,服务端⼀般是基于NIO(即同步非阻塞IO或IO多路复用)来实现长连接,性能可以极大的提升.一个线程在同时管理多个clientSocket.

举例说明:下楼上小摊买饭
一家三口,爸爸妈妈和孩子.三个人要吃不同的饭,第一种方案就是,三个人都下楼,各买各的.就是同步阻塞IO.
第二种方案就是只让爸爸一个人下去买,爸爸就可以在买完一份饭之后,在等待这个饭做出来的途中,再去买另一个饭,这样就会大大提高效率.就是IO多路复用.
在这里插入图片描述

评论 39
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值