C/S模型:TCP,UDP构建客户端和服务器端(BIO实现)

Java中提供了socket编程来构建客户端和服务器端

TCP

构建服务器端的步骤:
(1)bind:绑定端口号
(2)listen:监听客户端的连接请求
(3)accept:返回和客户端连接的实例
(4)read/write:进行读写操作,也就是和客户端进行交互
(5)close:关闭资源
Java中提供了ServiceSocket关键字来构建服务器,在Java中listen和accept合并为一个accept操作,下面通过代码演示一下这5个步骤

public class Server {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket();
        //bind:绑定ip和端口
        serverSocket.bind(new InetSocketAddress(6666));
        //listen监听并且accpet返回socket实例
        Socket accept = serverSocket.accept();
        //通过socket拿到输入流和输出流
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(accept.getInputStream()));
        PrintStream printStream = new PrintStream(accept.getOutputStream());
        //读到客户端发来的数据并打印
        String s = bufferedReader.readLine();
        System.out.println("收到客户端的数据:"+s);
        //向客户端输出数据
        printStream.println("啦啦啦啦服务器回消息:"+s);
        //关闭流和socket
        printStream.close();
        bufferedReader.close();
        serverSocket.close();
    }
}

我们也可以在创建ServerSocket的同时绑定端口号,也就是这样写

ServerSocket serverSocket = new ServerSocket(6666);

构建客户端的步骤:
(1)connect:通过IP地址和端口号连接服务器端
(2)read/write:进行读写操作,也就是和服务器端进行信息交流
(3)close:关闭资源
可以看到相比于服务器端,客户端的操作要简单很多
由于我们写的是TCP协议下的,所以要先启动服务器端,服务器端启动后,在accept会等待客户端的连接,也就是说代码在这里会阻塞住,客户端在这个时候连接就可以了。
下面用代码演示一下这几个步骤:

public class Client {
    public static void main(String[] args) throws IOException {
        Socket socket = new Socket();
        //通过ip和端口号连接服务器端
        socket.connect(new InetSocketAddress("127.0.0.1",6666));
        //定义读写流
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        PrintStream printStream = new PrintStream(socket.getOutputStream());
        //向服务器端发送数据
        printStream.println("哈哈哈,我是客户端");
        //接收服务器恢复的数据
        String s = bufferedReader.readLine();
        System.out.println(s);
        //关闭资源
        printStream.close();
        bufferedReader.close();
        socket.close();
    }
}

同样我们也可以在创建Socket的同时连接服务器端

Socket socket = new Socket("127.0.0.1",6666);

这两个demo都用到了缓冲流和打印流来进行读写操作,下面看一下运行的结果

在这里插入图片描述
在这里插入图片描述
下面通过画图来描述一下这个过程
在这里插入图片描述
通过这幅图也会很清楚的知道三次握手和四次挥手分别发生在什么时候

下面想一个需求,如果是多个客户端和服务器端进行信息的交流怎么办?

刚才已经提到了,服务器端accpet操作是等待客户端连接的操作,那么写一个循环,每一个循环体里面有一个accept不就可以解决多个客户端连接服务器端的问题了,但是要注意一点的是accpet是一个阻塞操作,所以需要多个线程才可以,下面解释一下为什么:
首先需要明白的是只需要对服务器端进行修改即可,所以这块都是针对服务器端说的
如果只有一个线程,那么通过accpet连接上,那么这个线程还要用来进行读写,在读写的时候也会阻塞,如果读写的时候阻塞了,那么下一个accpet就是不能正常连接的,线程就一直停到第一个连接上了,直到该连接完毕结束后才可以下一个连接,很明显这个过程是一个串行的过程,达不到所要的效果,这就需要多线程,主线程只用来保持连接(accpet),然后子线程负责和客户端进行读写交互,这样的话子线程在读写阻塞的时候是不会影响到主线程的。

下面先来看看多个客户端一个服务器端的代码:
下面的代码只展示服务器端,客户端和上面保持不变

public class MutileServer {
    public static void main(String[] args) {
    	//创建有3个固定数量线程的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        try {
            ServerSocket sockets = new ServerSocket(6666);
            System.out.println("服务器已启动,正在等待连接");
            while(true) {
                Socket accept = sockets.accept();
                System.out.println("客户端:"+accept.getInetAddress().getHostAddress());
                executorService.execute(new MyThread(accept));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
class MyThread implements Runnable {
    private Socket socket;
    private BufferedReader bufferedReader;
    private PrintStream printStream;
    public MyThread(Socket socket) {
        this.socket = socket;
        try {
            bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            printStream = new PrintStream(new BufferedOutputStream(socket.getOutputStream()));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        try {
            String s = bufferedReader.readLine();
            System.out.println("客户端发来消息 "+s);
            printStream.println("echo"+s);
            printStream.flush();
            bufferedReader.close();
            printStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

需要说明的几点:
(1)这块我用到了线程池,不用多次创建线程,方便统一管理并且高效。也可以单独创建多个线程,通过循环也是可以的。
(2)我写了一个MyThread类专门用来处理读写和客户端的信息交流,也就是子线程完成读写操作,主线程只用关心和服务器端的连接即可。
(3)用完资源后必须关闭,并且尽量不要抛异常,在当前方法中处理一下。

缺点

用BIO实现TCP客户端和服务器端是有缺点的,BIO是同步阻塞模型,在JDK1.4之前一直使用这种模型,但是想这样一个问题,客户端每过来一个连接服务器端就要有一个线程与之对应,那么客户端非常多的情况下,服务器端线程就会非常多,而且线程上下文切换也会成为一笔非常大的消耗,这就是BIO的缺点,如果要改善这种缺点,就需要引入NIO。

UDP

UDP不像TCP是可靠的连接,也就是会保证数据的正确送达,而UDP会以数据报的形式扩散数据,举一个很简单的例子:听广播,当节目到达时必须提前打开收音机,这样才会保证不会错过。UDP也是一样,服务器端发送数据时,客户端需要提前等着,以免错过数据。
UDP完成客户端和服务器端需要两个类:DatagramPacket和DatagramSocket
DatagramSocket是建立连接的套接字,可以理解为运送货物的码头
DatagramPacket是数据包,也就是说数据在传送过程中是被打包成了数据包,如果DatagramSocket是码头,那么DatagramPacket就是装载货物的箱子。

看一下代码:
Server:

public class Server {
    public static void main(String[] args) {
        //要发送的数据
        String data = "数据发送了。。。";
        byte[] bytes = data.getBytes();

        try {
            //包装成数据包的形式
            DatagramPacket packet = new DatagramPacket(bytes, bytes.length,InetAddress.getByName("127.0.0.1"), 8888);
            //初始化DatagramSocket
            DatagramSocket socket = new DatagramSocket();
            //发送数据
            socket.send(packet);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Client:

public class Client {
    public static void main(String[] args) {
        try {
            //选择监听的端口号
            DatagramSocket socket = new DatagramSocket(8888);
            //初始化接收数据包
            byte[] bytes = new byte[1024];
            DatagramPacket packet = new DatagramPacket(bytes, bytes.length);
            //接收数据
            socket.receive(packet);
            //解析数据包
            String s = new String(packet.getData(), 0,packet.getLength());
            //打印接收到的数据
            System.out.println(s);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

先启动客户端,然后再启动服务器
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_42220532/article/details/90376915