建立客户端和服务端互连简单的聊天操作

服务端:

package JAVA_API.num18_socket;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * 聊天室服务端
 *
 * @author yyc
 * 2021/9/28 14:44
 */
public class Server {
    /*
    * 运行在服务端的ServerSocket 主要有两个作用:
    * 1.向系统申请服务端口,客户端的Socket就是通过这个端口与服务端建立连接的。
    * 2.监听服务端口,一旦一个客户端通过改端口建立连接会自动创建一个Socket,服务端就可以通过这个Socket与客户交互了。
    *
    * 如果我们把Socket比喻为电话,nameServerSocket相当于是某客服中心的总机。
    * */
    private ServerSocket serverSocket ;

    /*
    * 创建一个线程安全的集合
    * 线程安全的集合中,add,remove 等方法上都直接使用了synchronized修饰,多个线程是不能同时调用一个集合的这些方法的。
    * 保证了同步执行
    *
    * */
    //建一个集合用来存放通过流写出给的客户数量,这个集合应是一个线程安全的.
    private List<PrintWriter> allOutput = Collections.synchronizedList(new ArrayList<>());

    //进行服务端的初始化:
    public Server(){
        try {
            System.out.println("正在启动服务端...");
            /*
            * 实例化时要指定服务端口,如果该端口被当前系统其他应用程序占用时,会报异常:
            * java.net.BindException: Address already in use: JVM_Bind(IDEA端口号:)
            * */
            //初始化时给成员变量赋值,在()里填入数字申请端口号,用来连接服务器
            //IllegalArgumentException: Port value out of range: 65536
            //serverSocket = new ServerSocket(65536);
            serverSocket = new ServerSocket(5050);
            System.out.println("服务端启动完毕!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public void start() {
        try {
            /*
             * ServerSocket提供的方法:
             * Socket accept();该方法是一个阻塞方法,调用后程序进入阻塞状态,直到一个客户端实例化Socket与当前服务端建立连接,
             * 此时accept()方法会返回一个Socket实例,服务端通过他就可以与客户端交互了。
             *
             * 可以理解为这个动作相当于是总机的"接电话"操作 。
             *
             * */
                while (true) {
                    System.out.println("等待客户端连接...");
                    //然后在main方法里调用方法,先启服务端,在启动客户端,accept()获取到的socket的返回值是连接当前服务端的客户端的socket。客户端的socket是Output来的。
                    Socket socket = serverSocket.accept();//用来监听
                    System.out.println("一个客户端连接了...");
                    //启动一个线程来负责与客户端交互的操作:
                    //1.创建线程任务,传入的参数就是监听到的Socket,
                    ClientHandler handler = new ClientHandler(socket);
                    //2.创建一个线程并执行该任务
                    Thread thread1 =  new Thread(handler);
                    //3.启动线程
                    thread1.start();
                }
            } catch(IOException e){
                e.printStackTrace();
            }
        }

        /*
        * 该线程任务用于指定的客户端进行交互。
        * 每个连接服务端的客户端都是通过该线程进行交互的。
        * 即:一个客户端靠一个线程进行交互
        * */
        //写一个内部类来定义任务
        private class ClientHandler implements Runnable {
            //内部类可以有成员方法和变量
            //声明一个成员变量和构造方法
            private Socket socket;
            private String host;//当前线程处理的客户端的地址信息

            //通过传入的socket获取客户端的信息。
            public ClientHandler(Socket socket){
                this.socket = socket;
                //通过socket获取远端计算机地址信息(对于服务器而言,远端就是客户端),地址可以用来拼上谁谁说的话
                //获取客户端地址就是start()方法中监听到的socket即是客户端ip及端口,
                host = socket.getInetAddress().getHostAddress();
            }
            @Override
            public void run() {
                //将pw放在这里是为了能在finally中看到并处理
                PrintWriter pw = null;
                //socket能获取的只有字节流
                try {
                    /*
                    * Socket提供的方法:
                    * InputStream getInputStream();
                    * 通过该方法获取的字节输入流读取的是远端计算机发送过来的数据。
                    *
                    * 这里相当于读取当前服务端中这个Socket对应的远端(客户端)
                    * 那边Socket获取的输出流写出的字节数据
                    * */
                    InputStream inputstream = socket.getInputStream();
                    //想要读,先转换
                    InputStreamReader isr = new InputStreamReader(inputstream,"UTF-8");
                    BufferedReader br = new BufferedReader(isr);

                    //用这个流来写出数据给客户端
                    pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(),"UTF-8")),true);
                    /*
                    * 将该消息存入共享集合allOut中,便于其他ClientHandler将消息发送给当前客户端。
                    * 由于是线程安全的集合,不必自行添加锁机制即可保证线程同步调用
                    * */
                    allOutput.add(pw);
                    //host是客户地址,allOutput.size()客户上线人数
                    System.out.println(host + "上线了,当前的在线人数:" + allOutput.size());
                    //调用发送消息的方法把消息发送给所有客户
                    sendMessage(host + "上线了,当前的在线人数:" + allOutput.size());
                    String line ;
                    while((line = br.readLine()) != null){

                        //host是读到的客户端地址,读到的内容是line
                        System.out.println(host + "说:" + line);

                        //将消息回复给所有的客户端,想要实现群发的效果,可以把写出到这一句放到集合,需要把客户端放到集合里
                        //pw.println(line);不能实现群发
                        //将客户输入的信息发给所有用户
                        sendMessage(host + "说:" + line);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }finally {
                    //处理客户端断开后的操作
                    //1.从集合中移除
                    allOutput.remove(pw);
                    //移除之后给一个下线的通知
                    //host是客户地址,allOutput.size()客户上线人数
                    //2.广播下线通知
                    System.out.println(host + "下线了,当前的在线人数:" + allOutput.size());
                    try {
                        //3.关闭socket释放资源
                        socket.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
            //发送消息的方法
            /**
             * 将消息转发给所有客户端(发送给集合里的每一个人,遍历集合)
             * @pararm message 要发送的内容
             * */
            private void sendMessage(String message){
                /**
                 * 遍历集合的每一个输出流,把消息发送出去(给所有客户端)
                 * */
                allOutput.forEach(pw -> pw.println(message));
              /*  for (int i = 0 ; i < allOutput.size(); i++){
                    allOutput.get(i).println(message);
                }*/
            }
        }

    //main方法用于测试
    public static void main(String[] args) {
        Server server = new Server();
        server.start();
    }
}

客户端:

package JAVA_API.num18_socket;

import java.io.*;
import java.net.Socket;
import java.util.Scanner;

/**
 * 聊天室客户端
 * 想要实现网络通信需要一个Socket套接字(Socket是一个应用程序接口(应用程序接口又叫API,即Socket是一个interface提供了一个方法使用))
 * 有了这样一种接口技术,一台计算机就可以通过软件的方式与任何一台具有Socket接口的计算机进行通信。端口在计算机编程上也就是"Socket接口"。
 * @author yyc
 * 2021/9/28 14:36
 */
public class Client {
    /*
    TCP和UDP协议:
    TCP(Transmission Control Protocol):安全性可靠性更高
     UDP协议:不安全,实时性。
    * java.util.Socket套接字(客户端套接字)
    * Socket封装了TCP协议的通讯细节,使得我们使用它可以 与服务端建立网络连接,并通过他获取两个流(一个输入一个输出),
    * 然后使用这两个流的读写操作完成与服务端的数据交互。
    * */
    private Socket socket;
    /**/
    public Client(){
        /*
        * 通过Win + ipconfig 查本机host
        * 实例化Socket时通常需要传入两个参数:
        *  public Socket(String host, int port)
        * 参数1为服务端口的地址信息(IP地址,如果连本机可用loacalhost)
        * 参数2为服务端打开的服务端口,即:服务端ServerSocket申请的端口
        * */
        try {
            System.out.println("正在连接服务器...");
            socket = new Socket("192.168.43.196",5050);
            System.out.println("成功连接服务器!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /*
    * 客户端开始工作的 方法:用户发消息客户端与服务器做交互
    * Socket提供输入流供我们输入
    * */
    public void start(){
        //在写之前也需要
        //客户端启动后,先启动一个线程来读取服务端发送过来的消息
        ServerHandler handler = new ServerHandler();
        Thread thread = new Thread(handler);
        thread.start();
        /*
        Socket 提供的方法:
        OutputStream getOutputStream();该方法会获取一个字节输出流,通过这个输出流写出的字节数据会通过网络法送给对方。
         */

        //把流放在try()
        try(PrintWriter pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(),"UTF-8")),true);
        ){
            //定义一个输出流用于写出数据到
            //OutputStream outputStream = socket.getOutputStream();
            //PrintWriter按行写出,BufferedWriter缓冲字符输出流加快写出数据,OutputStreamWriter将字符转换成字节,
            System.out.println("开始聊天吧!单独输入exit是退出!");
            Scanner scanner = new Scanner(System.in);
            while(true){
                String line = scanner.nextLine();
                if ("exit".equals(line)){
                    System.out.println("聊天结束");
                    break;
                }
                //将客户端控制台输入的信息发送到服务端,通过pw.Println()写出到服务端
                pw.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally{
            try {
                socket.close();//最终和对方(服务器)断开连接
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    /**
     * 该线程用来负责循环接收服务端发送过来的消息
     * */
    private class  ServerHandler implements Runnable {
        @Override
        public void run(){
            try (BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream(),"UTF-8"))){
               //用来接收流读取出来的服务器发送过来的信息
                String message;
                //读取服务器发送过来的一行字符串
                while((message = br.readLine()) != null){
                    System.out.println(message);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    //测试
    public static void main(String[] args) {
        /*
        * java.net.ConnectException: Connection refused: connect
        * 拒接链接,因为此时服务端还没有接受客户端连接
        * */
        Client client = new Client();
        client.start();
    }
}

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
客户端开发文档 项目类别:基于对话框的MFC程序。 项目名称:MFCClient 一、 程序的初始化。 1、 在函数CMFCClientDlg::AddInit()中进行相应的操作。主要功能为对控件进行初始化。禁用控件和设置空间初始值。 2、 在CMFCClientDlg::InitInstance()中加载套接字库 二、 客户端单击“连接服务器”按钮后的操作。 1、 创建一个新线程,用于处理相关的SOCKET操作。线程函数为CMFCClientDlg的静态成员函数fnSocket(). 2、 fnSocket()的主要作用。 a、 使用函数CMFCClientDlg::CheckName(CString)对昵称进行检查 b、 创建套接字、检查输入的端口、与服务端进行连接以及循环接收来自服务端的消息。 c、 将应当禁用和开启的控件分别进行操作。 3、使用DisposeRecvMsg(Cstring str)对收到的消息进行处理。 三、 客户端单击“发送消息”按钮后的操作。 从文本控件中获得Cstring类型的数据,与想发送的目标客户端昵称根据之前的约定进行组合,然后转换为TCHAR的数据,再发送到所有的客户端。 四、 客户端单击“断开服务器”按钮后的操作 关闭套接字,此时,服务端将会收到关闭消息,从而作出相应的相应。 服务端开发文档 项目类别:基于对话框的MFC程序。 项目名称:MFCServer 一、 程序的初始化。 1、 在函数CMFCServerDlg::AddInit()中进行相应的操作。主要功能为对控件进行初始化。禁用控件和设置空间初始值。 2、 在CMFCServerApp::InitInstance()中加载套接字库 二、 服务端单击“开启服务器”按钮后的操作。 1、 创建一个新线程,用于处理相关的SOCKET操作。线程函数为CMFCServerDlg的静态成员函数fnSocket(). 2、 fnSocket()的主要作用。 a、 创建套接字、检查输入的端口、绑定套接字、监听端口以及使用select模型开始处理套接字。 b、 将应当禁用和开启的控件分别进行操作。 3、 维护用于存储客户端昵称和套机字的Carray数组m_ClientMap 4、 使用函数DisposeRecvMsg(SOCKET s,Cstring str)来处理消息接收到的消息。 三、 服务端单击“停止服务器”按钮后的操作。 清空相关的数据和对相关控件的禁用进行操作。 四、 服务端单击“发送消息”按钮后的操作。 从文本控件中获得Cstring类型的数据,然后转换为TCHAR的数据,再发送到所有的客户端
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值