弄懂服务端、客户端【手写一个socket聊天室】

常见架构:

C/S架构:Client/Server(客户端/服务器)结构在这里插入图片描述
B/S架构:Browser/Server(浏览器/服务器)结构
在这里插入图片描述

服务端和客户端

        简单地说:一般客户端负责和用户的交互,也就是屏幕显示(UI/UE),服务端负责数据存储,也就是你的用户数据,而计算能力,客户端和服务端一般各负责一部分。
        微信、qq这种聊天功能一般在用户之间通信时,用户的数据先是发送到服务器上的,然后通过服务器进行转发给指定用户,从而完成一次用户间的通信,那么如何证明这一点呢?比如一下几种场景:

  1. 当A用户不在线时,B用户给A发送,A是接收不到的,当A一上线,信息会立马发送给他,可推理出B用户发送的数据会保存在服务器中。
  2. 当A用户给B传输文件时,上传完后,B需要下载才可传输到本地,说明下载之前已经上传到服务器了,如果清空本地的该文件,在一定时间范围内仍然可以重新下载回来,说明服务器上文件还在。
            对于一个简单的群聊功能,实现的基本原理就是,客户端给服务器发送数据,服务器再将请求发送给其他客户端,从而完成转发,单聊那就是在客户端传服务器的过程中加入了目标用户识别码。

TCP/IP、UDP?

        TCP/IP(Transmission Control Protocol/Internet Protocol)即传输控制协议/网间协议,是一个工业标准的协议集,它是为广域网(WANs)设计的。
        UDP(User Data Protocol,用户数据报协议)是与TCP相对应的协议。它是属于TCP/IP协议族中的一种。
        这里有一张图,表明了这些协议的关系。 TCP/IP协议族包括运输层、网络层、链路层。
在这里插入图片描述

Socket技术

在这里插入图片描述
        Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
        简单来说,socket就是对tcp/ip协议的封装,在java中是一个类,方便用户去完成通信。

在这里插入图片描述

Socket建立通信的流程:

        服务端的ServerSocket通过绑定ip和port,从而完成初始化,以保证在port下监听待接入的客户端,accept()函数使得监听到接入的客户端,这里可以采用死循环【建立新的线程】保持服务端一直在监听并接受新的客户端的连接。

ServerSocket ss = new ServerSocket(port);
Socket socket = ss.accept();

客户端:
同样通过绑定ip port建立Socket

socket = new Socket(host, port);

服务端accept得到的Socket相当于是在客户端自建的Socket基础之上构建的新的Socket

服务端得到了与客户端连接的Socket之后便可以拿到该Socket的输入、输出流;
对于客户端也是一样,需要通过Socket拿到输入输出流

InputStream is= socket.getInputStream();
OutputStream os = socket.getOutputStream();

群聊代码:

思路:
在这里插入图片描述

        服务端和客户端通信必须构建一个管道,管道的两端分别是InputStream、OutputStream,当客户端输出时用OutputStream,某一个客户端对应的就是InputStream来接受客户端的输出,即每个管道的输入输出一一对应。
        那么对于服务端来说,他的输入流,应该是客户端的输出流,有多少个客户端,就应该有多少个线程来维护这个管道,所以每连接一个客户端,就应该启动一个输入流给服务端。
        那么,服务端的输出流(输出的线程)应该有多少个呢,如果要实现转发 群发功能,那么服务端的输出流只能是一个,因为它要给每一个客户端去发消息,消息可以存在消息列表里面,每次从msgQueue里面拿出第一个消息,然后发送给每一个客户端,所以对于服务端的输出线程,它理应包含一个list去装载每一个客户端的socket,这样才能在群发时 遍历每一个客户端的socket 并且拿到socket对应的输出流,将消息群发出去。
        对于1个客户端来说,比服务端简单很多,输入流(输入线程)就只用构建一个,用于接受客户端输出的数据,输出流(输出线程)也是构建一个,用于发送给服务端。具体的代码架构如下图所示,将客户端和服务端代码分离,便于维护和后期拓展功能
在这里插入图片描述

客户端:

package chatroom.client;

import chatroom.client.InputThread;
import chatroom.client.OutputThread;

import java.io.IOException;
import java.net.Socket;

/**
 * @author :erickun
 * @date :Created in 2021/8/1 10:42 上午
 * @description:
 * @modified By:
 * @version: $
 */
public class Client {

    //定义Socket对象
    Socket socket = null;
    //服务器的Ip地址和服务器所使用的端口号
    private String host = "localhost";
    private int port = 8082;
    private String label = "客户端";
    private String clientName;

    public Client(String clientName) {
        this.clientName = clientName;
        createCient();
    }

    private void createCient() {

        Socket socket = null;
        try {
            socket = new Socket(host, port);
            createInput(socket,label);
            createOutput(socket,label);

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


    }

    private void createOutput(Socket socket, String label) {
        OutputThread outputThread = new OutputThread(socket,label);
        outputThread.setClientName(clientName);
        new Thread(outputThread).start();
    }

    private void createInput(Socket socket, String label) {
        InputThread inputThread = new InputThread(socket,label);
        new Thread(inputThread).start();
    }



    public static void main(String[] args) {
        Client client = new Client("客户端1");
        Client client2 = new Client("客户端2");

    }


}
package chatroom.client;

import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.util.concurrent.LinkedBlockingDeque;

/**
 * @author :erickun
 * @date :Created in 2021/8/1 1:24 下午
 * @description:
 * @modified By:
 * @version: $
 */
public class InputThread implements Runnable {
    Socket socket = null;
    private String label;

    public InputThread(Socket socket, String label) {
        this.socket = socket;
        this.label = label;
    }
    
    @Override
    public void run() {
        InputStream is = null;

        try {
            is = socket.getInputStream();

            while (true) {
                byte[] b = new byte[1024];
                int len = is.read(b);

                //客户端输入:接受服务端的输出
                System.out.println(new String(b, 0, len));

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


package chatroom.client;

import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.HashMap;
import java.util.Scanner;
import java.util.concurrent.LinkedBlockingDeque;

/**
 * @author :erickun
 * @date :Created in 2021/8/1 1:26 下午
 * @description:
 * @modified By:
 * @version: $
 */
public class OutputThread implements Runnable {
    Socket socket = null;
    private String label; // 记录是客户端 or 服务端

    private String clientName; //记录客户端的名称

    public OutputThread(Socket socket, String label) {
        this.socket = socket;
        this.label = label;
    }

    public void setClientName(String clientName) {
        this.clientName = clientName;
    }

    @Override
    public void run() {
        OutputStream os = null;
        try {
            os = socket.getOutputStream();
            //客户端 接受用户的输入 发送到服务端
            System.out.println("请输入要发送的内容");
            Scanner ss = new Scanner(System.in);
            String ans = null;
            while (true) {
                long curTime = System.currentTimeMillis();
                curTime = curTime;
                ans = clientName + ":" + ss.nextLine();

                os.write(ans.getBytes());
            }
        } catch (
                IOException e) {
            e.printStackTrace();
        }
    }
}

package chatroom.client;

import chatroom.client.Client;

/**
 * @author :erickun
 * @date :Created in 2021/8/1 3:42 下午
 * @description:客户端1
 * @modified By:
 * @version: $
 */
public class ClientTest1 {

    public static void main(String[] args) {
        Client client = new Client("客户端1");
    }
}

package chatroom.client;

import chatroom.client.Client;

/**
 * @author :erickun
 * @date :Created in 2021/8/1 3:42 下午
 * @description:
 * @modified By:
 * @version: $
 */
public class ClientTest2 {
    public static void main(String[] args) {
        Client client = new Client("客户端2");
    }
}

服务端:

package chatroom.server;

import chatroom.server.InputThread;
import chatroom.server.OutputThread;


import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.concurrent.LinkedBlockingDeque;

/**
 * @author :erickun
 * @date :Created in 2021/8/1 10:43 上午
 * @description:
 * @modified By:
 * @version: $
 */
public class Server {

    private int port = 8082;
    private String label = "服务端";
    Socket socket = null;
    public LinkedBlockingDeque<String> msgQueue;
    public ArrayList<Socket> sockets;//维护每一个与客户端连接的socket
    public Integer clientNum;

    public Server() {
        createServer();
        this.clientNum = sockets.size();
    }

    public void createServer() {

        try {
            //服务器套接字
            ServerSocket ss = new ServerSocket(port);
            System.out.println(("服务器已经启动,监听端口为" + port));
            //初始化一个msgQueue用于存放客户端传来的数据
            this.msgQueue = new LinkedBlockingDeque<String>();
            this.sockets = new ArrayList<Socket>();

            createOutput(sockets, msgQueue);
            // 给客户端发送信息 只需要维护一个OutputThread即可 以保证每次从msgQueue里取一次msg
            // 都能将该msg群发个多个客户端

            while (true) {

                Socket socket = ss.accept();
                sockets.add(socket);
                createInput(socket, msgQueue);//每一个与客户端之间的socket都需要建立一个inputThread去接收

                //服务器套接字等待一个客服端socket连入,如果连接成功的话,就会创建一个套接字,不然在这里一直等待
                System.out.println("已经接受连接");

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

    }

    //线程 OutputThread:服务端输出到客户端
    public void createOutput(ArrayList<Socket> sockets, LinkedBlockingDeque<String> msgQueue) {

        OutputThread outputThread = new OutputThread(msgQueue, sockets);
        new Thread(outputThread).start();

    }

    //线程 InputThread:服务端接受客户端的输入
    public void createInput(Socket socket, LinkedBlockingDeque<String> msgQueue) {

        InputThread inputThread = new InputThread(socket, msgQueue);
        new Thread(inputThread).start();

    }

    public static void main(String[] args) {
        Server server = new Server();

    }

}

package chatroom.server;

import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.util.concurrent.LinkedBlockingDeque;

/**
 * @author :erickun
 * @date :Created in 2021/8/1 1:24 下午
 * @description:
 * @modified By:
 * @version: $
 */
public class InputThread implements Runnable{
    Socket socket = null;
    private String label;
    public   LinkedBlockingDeque<String> msgQueue;

    public InputThread(Socket socket,LinkedBlockingDeque<String> msgQueue){
        this.socket = socket;
        this.msgQueue = msgQueue;
    }

    public void setMsgQueue(LinkedBlockingDeque<String> msgQueue) {
        this.msgQueue = msgQueue;
    }

    @Override
    public void run() {
        InputStream is = null;

        try {
            is= socket.getInputStream();

            while (true){
                byte[] b = new byte[1024];
                int len = is.read(b);
                //接受客户端传来的信息
                System.out.println(new String(b , 0, len));
                //再把信息传到集合中去,再在outputThread中输出给其他客户端
                msgQueue.add(new String(b , 0, len));

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

package chatroom.server;

import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Scanner;
import java.util.concurrent.LinkedBlockingDeque;

/**
 * @author :erickun
 * @date :Created in 2021/8/1 1:26 下午
 * @description:
 * @modified By:
 * @version: $
 */
public class OutputThread implements Runnable {
    private ArrayList<Socket> sockets;
    public LinkedBlockingDeque<String> msgQueue;
    private String label; // 记录是客户端 or 服务端

    public OutputThread(LinkedBlockingDeque<String> msgQueue, ArrayList<Socket> sockets) {
        this.sockets = sockets;
        this.msgQueue = msgQueue;
    }

    @Override
    public void run() {
        OutputStream os = null;
        try {
            while (true) {
                if (msgQueue.size() > 0) {
                    String poll = msgQueue.poll();
                    //群发给所有客户端
                    for (int i = 0; i < sockets.size(); i++) {
                        Socket curSocket = sockets.get(i);
                        os = curSocket.getOutputStream();
                        os.write(poll.getBytes());
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值