Socket客户端-服务器(C-S)通信实验

1.需求分析

设计程序,分别构建通信的两端:服务器端和客户端应用程序,套接字类型为面向连接的Socket,自己构建双方的应答模式,实现双方的数据的发送和接收(S发给C,C发给S)。
服务端程序能响应单个或任意多个客户端连接请求;服务端能向单个客户发送消息,支持群发消息给所有客户端;通信的双方具备异常响应功能,包括对方异常退出的处理。如果客户端退出,服务器有响应;反之亦然。
编程实现的思路:在服务器端,首先要启动一条线程用于监听某个指定端口(比如:8000),并且还要再开一条线程用于接收消息,客户端尝试连接该端口(8000),如果成功连接则会返回一个Socket类的实例对象(Socket socket = serversocket.accept()😉 ,很显然服务器端便应该保存有一个客户列表(比如可以用:ArrayList),使得服务器可以发消息给某个指定的客户端。而客户端在连接服务器之后也应该启动一条线程用于接收消息。当某个客户端进来时,便发送Login消息给服务器,服务器将此消息广播发给当前在线的所有用户,当某个客户端退出时,发送Logout消息给服务器,服务器将此消息广播发给当前在线的所有用户,当客户端之间进行通信时,会发送Talk消息给服务器,服务器再将此消息转发给指定的客户端,也就是所有的通信都是通过服务器进行转发的。

1. 创建JAVA应用程序工程

如图
在这里插入图片描述

2. 1创建UI视图类

2.1.1建立ClientView类

在这里插入图片描述
创建成功:

public class ClientView extends JFrame implements{

}

2.1.2实现处理交互事件的接口

让ClientView类实现ActionListener, KeyListener接口。并使用IDE自动生成接口中的方法。

public class ClientView extends JFrame implements ActionListener, KeyListener{

	@Override
	public void keyPressed(KeyEvent e) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void keyReleased(KeyEvent e) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void keyTyped(KeyEvent e) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void actionPerformed(ActionEvent e) {
		// TODO Auto-generated method stub
		
	}

}

keyPressed():处理键盘按下事件
actionPerformed():处理鼠标点击事件

2.1.3UI元素初始化

首先,在类中添加如下成员变量:

    private JTextArea taChatList;   // 聊天内容区
    private JTextField tfMessage;   // 聊天输入框
    private JTextField tfName;      // 用户名输入框
    private JButton btnSend;        // 发送按钮
    private JLabel labelNick;        
    private JPanel jp1, jp2;

    private JScrollPane scrollPane;
    private JLabel labelHost;       
    private JLabel labelPort;
    private JTextField tfHost;      // 服务器地址输入框
    private JTextField tfPort;      // 服务器端口输入框
    private JButton btnConnect;     // 连接/断开服务器按钮

接下来,编写一个用户界面初始化函数initView(),对各个UI元素对象分配存储空间,并按照设计要求添加到视图中。代码如下:

    private void initView() {
        taChatList = new JTextArea(20, 20);
        taChatList.setEditable(false);

        scrollPane = new JScrollPane(taChatList);
        tfMessage = new JTextField(15);
        btnSend = new JButton("发送");

        jp1 = new JPanel();
        labelHost = new JLabel("主机地址");
        tfHost = new JTextField(15);
        tfHost.setText("localhost");
        labelPort = new JLabel("端口号");
        tfPort = new JTextField(4);
        tfPort.setText("8765");
        btnConnect = new JButton("连接");

        jp1.add(labelHost);
        jp1.add(tfHost);
        jp1.add(labelPort);
        jp1.add(tfPort);
        jp1.add(btnConnect);

        labelNick = new JLabel("昵称:");
        tfName = new JTextField(8);
        jp2 = new JPanel();
        jp2.add(labelNick);
        jp2.add(tfName);
        tfName.setText("用户0");
        jp1.setLayout(new FlowLayout(FlowLayout.CENTER));
        jp2.add(tfMessage);
        jp2.add(btnSend);
        jp2.setLayout(new FlowLayout(FlowLayout.CENTER));

        add(jp1, BorderLayout.NORTH);
        add(scrollPane, BorderLayout.CENTER);
        add(jp2, BorderLayout.SOUTH);
        setTitle("聊天室");
        setSize(500, 500);
        setLocation(450, 150);
        setVisible(true);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        
        // 当光标定位在聊天输入框时监听回车键按下事件
        tfMessage.addKeyListener(this);
        // 为发送按钮增加鼠标点击事件监听
        btnSend.addActionListener(this);
        // 为连接按钮增加鼠标点击事件监听
        btnConnect.addActionListener(this);
         // 当窗口关闭时触发
        addWindowListener(new WindowAdapter() { // 窗口关闭后断开连接
            @Override
            public void windowClosing(WindowEvent e) {
                
            }
        });
    }

找到keyPressed()方法,添加对按下回车键事件的处理:

    @Override
    public void keyPressed(KeyEvent e) {
        // TODO Auto-generated method stub
        if (e.getKeyCode() == KeyEvent.VK_ENTER) {
            // 发送聊天消息
        }
        
    }

找到actionPerformed()方法,添加对两个按钮的响应:

    @Override
    public void actionPerformed(ActionEvent e) {
        // TODO Auto-generated method stub
        if (e.getSource() == btnSend) {
            // 响应发送按钮
        } else if (e.getSource() == btnConnect) {
            // 响应连接/断开按钮
        }
    }

最后,为ClientView创建一个构造方法,并且在构造方法中调用initView()方法对用户界面进行创建:

    public ClientView() {
        initView();
    }

2.1.4运行程序查看主窗口

在ClientView类的末尾增加主函数作为程序运行的入口:

    public static void main(String[] args) {
        ClientView view = new ClientView();
    }

运行结果如图:
在这里插入图片描述

2.2 编写网络服务模块

2.2.1 创建Networkservice类

在这里插入图片描述
创建成功:

public class NetworkService {

}

2.2.2 定义NetworkService模块的功能

connnect():连接到服务器
disconnect():断开与服务器的连接
isConnected():判断当前是否已经连接到服务器
sendMessage():发送聊天消息

为NetworkService类添加与这些功能相对应的方法:

public class NetworkService {

    /**
     * 连接到服务器
     * @param host 服务器地址
     * @param port 服务器端口
     */
    public void connect(String host, int port) {
        
    }
    /**
     * 断开连接
     */
    public void disconnect() {
        
    }
    /**
     * 是否已经连接到服务器
     * @return true为已连接,false为未连接
     */
    public boolean isConnected() {
    }
    /**
     * 发送聊天消息
     * @param name 用户名
     * @param msg 消息内容
     */
    public void sendMessage(String name, String msg) {
    }
}

2.2.3 定义回调接口

以上网络功能操作完成后,往往需要反馈一些状态,例如在收到消息后通知用户界面刷新聊天内容,使得用户能够看到新消息。而根据分层设计的原理,这样的代码放在专注于网络操作的NetworkService类中是非常丑陋的做法。因此,我们定义一个回调接口,其中定义各项处理结束的通知函数。稍后我们在用户界面类(即ClientView类)中实现这个接口,就可以实现UI对网络处理的响应了。
在NetworkService类的最前面中添加接口Callback的定义,同时用Callback类型定义一个成员变量,并创建setter方法:

public class NetworkService {
    public interface Callback {
        void onConnected(String host, int port);        //连接成功
        void onConnectFailed(String host, int port);    //连接失败
        void onDisconnected();                          //已经断开连接
        void onMessageSent(String name, String msg);    //消息已经发出
        void onMessageReceived(String name, String msg);//收到消息
    }
    
    private Callback callback;
    public void setCallback(Callback callback) {
        this.callback = callback;
    }
        ...
}

2.2.4 添加网络通信相关的成员变量

添加网络通信所需要的以下成员变量:

    // 套接字对象
    private Socket socket = null;
    // 套接字输入流对象,从这里读取收到的消息
    private DataInputStream inputStream = null;
    // 套接字输出流对象,从这里发送聊天消息
    private DataOutputStream outputStream = null;
    // 当前连接状态的标记变量
    private boolean isConnected = false;

2.2.5 实现connect()操作

connect()方法实现连接服务器的操作。它的逻辑如下:

  • 根据参数提供的服务器地址和端口创建套接字。创建套接字的过程即建立连接的过程。
  • 如果创建成功,记录已连接状态,同时通过回调函数通知外界连接成功。同时还要启动一个线程来监听是否有服务器发来的聊天消息。
  • 如果创建套件字失败,则记录未连接状态,通过回调函数通知外界连接失败

为connect()方法编写代码如下:

    public void connect(String host, int port) {
        try {
            // 创建套接字对象,与服务器建立连接
            socket = new Socket(host, port);
            isConnected = true;
            // 通知外界已连接
            if (callback != null) {
                callback.onConnected(host, port);
            }
            // 开始侦听是否有聊天消息到来
            beginListening();
        } catch (IOException e) {
            // 连接服务器失败
            isConnected = false;
            // 通知外界连接失败
            if (callback != null) {
                callback.onConnectFailed(host, port);
            }
            e.printStackTrace();
        }
    }

其中,用来监听聊天记录到来的方法beginListening()实现如下:

    private void beginListening() {
        Runnable listening = new Runnable() {
            @Override
            public void run() {
                try {
                    inputStream = new DataInputStream(socket.getInputStream());

                    while (true) {
                        String[] s = inputStream.readUTF().split("#");
                        if (callback != null) {
                            callback.onMessageReceived(s[0], s[1]);
                        }
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        };
        (new Thread(listening)).start();
    }

2.2.6 实现disconnect()操作

disconnect()的功能是断开与服务器的连接。在这里要关闭套接字,并且关闭所有的输入、输出流。实现代码如下:

    public void disconnect() {
        try {
            if (socket != null) {
                socket.close();
            }
            if (inputStream!= null) {
                inputStream.close();
            }
            if (outputStream != null) {
                outputStream.close();
            }
            isConnected = false;
            // 通知外界连接断开
            if (callback != null) {
                callback.onDisconnected();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

2.2.7 实现isConnected()

外界通过调用isConnected()方法来获知当前是否已经连接到服务器。简单的返回isConnected变量即可:

    public boolean isConnected() {
        return isConnected;
    }

2.2.8 实现sendMessage()操作

sendMessage()方法将参数传来的用户名和消息串按照一定的格式发送出去。操作的实质就是将消息写入到套接字对象的输出流。实现如下:

    public void sendMessage(String name, String msg) {
        // 检查参数合法性
        if (name == null || "".equals(name) || msg == null || "".equals(msg)) {
            return;
        }
        if (socket == null) {   //套接字对象必须已创建
            return;
        }
        
        try {
            // 将消息写入套接字的输出流
            outputStream = new DataOutputStream(socket.getOutputStream());
            outputStream.writeUTF(name + "#" + msg); 
            outputStream.flush();
            // 通知外界消息已发送
            if (callback != null) {
                callback.onMessageSent(name, msg);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

2.3 用户界面与网络服务模块对接

2.3.1 为ClientView类增加NetworkService模块并实现回调

重新打开ClientView.java文件进行修改。
为了在ClientView类中对网络服务模块即NetworkService类进行调用,首先必须在ClientView类中增加一个NetworkService类型的成员:

然后专门写一个函数initNetworkService()来初始化这个networkService对象。这个函数主要做两件事:

  • 创建networkService对象(new)
  • 设置回调接口以处理NetworkService中各个网络操作发来的通知

initNetworkService()函数实现如下:

    private void initNetworkService() {
        networkService = new NetworkService();
        networkService.setCallback(new Callback() {
            @Override
            public void onConnected(String host, int port) {
                // 连接成功时,弹对话框提示,并将按钮文字改为“断开”
                alert("连接", "成功连接到[" + host + ":" + port + "]");
                btnConnect.setText("断开");
            }

            @Override
            public void onConnectFailed(String host, int port) {
                // 连接失败时,弹对话框提示,并将按钮文字设为“连接”
                alert("连接", "无法连接到[" + host + ":" + port + "]");
                btnConnect.setText("连接");
            }

            @Override
            public void onDisconnected() {
                // 断开连接时,弹对话框提示,并将按钮文字设为“连接”
                alert("连接", "连接已断开");
                btnConnect.setText("连接");
            }

            @Override
            public void onMessageSent(String name, String msg) {
                // 发出消息时,清空消息输入框,并将消息显示在消息区
                tfMessage.setText("");
                taChatList.append("我(" + name + "):\r\n" + msg + "\r\n");
            }

            @Override
            public void onMessageReceived(String name, String msg) {
                // 收到消息时,将消息显示在消息区
                taChatList.append(name + ":\r\n" + msg + "\r\n");
            }
        });
    }

其中,alert()函数用来显示一个对话框以向用户通告某个信息,实现如下:

    // 显示标题为title,内容为message的对话框
    private void alert(String title, String message) {
        JOptionPane.showMessageDialog(this, message, title, JOptionPane.INFORMATION_MESSAGE);
    }

然后找到构造方法ClientView(),在其末尾调用这个函数如下:

    public ClientView() {
        initView();
        initNetworkService();
    }

2.3.2 实现用户交互

不同的用户交互将会触发相应的网络操作,包括:

  • 未连接状态下,点击连接/断开按钮执行连接操作
  • 已连接状态下,点击连接/断开按钮执行断开连接操作
  • 已连接状态下,关闭窗口执行断开连接操作
  • 按回车键发送消息

下面分别调用NetworkService模块提供的功能来完成以上的交互操作。

2.3.2.1 处理关闭窗口操作

在ClientView类中找到如下代码:

         // 当窗口关闭时触发
        addWindowListener(new WindowAdapter() { // 窗口关闭后断开连接
            @Override
            public void windowClosing(WindowEvent e) {  
            }
        });

增加断开连接操作,如下:

         // 当窗口关闭时触发
        addWindowListener(new WindowAdapter() { // 窗口关闭后断开连接
            @Override
            public void windowClosing(WindowEvent e) {
                networkService.disconnect();
            }
        });

2.3.2.2 处理按钮点击操作

其中包括对连接/断开按钮的点击,以及对发送按钮的点击。前者连接或者断开服务器,后者将编辑框中的消息发送出去。
找到actionPerformed()方法,改写如下:

    @Override
    public void actionPerformed(ActionEvent e) {
        // TODO Auto-generated method stub
        if (e.getSource() == btnSend) {
            sendMessage();
        } else if (e.getSource() == btnConnect) {
            // 响应连接/断开按钮
            if (!networkService.isConnected()) {
                // 未连接状态下,执行连接服务器操作
                String host = tfHost.getText();
                int port = Integer.valueOf(tfPort.getText());
                networkService.connect(host, port);
            } else {
                // 已连接状态下,执行断开连接操作
                networkService.disconnect();
            }
        }
    }

其中,sendMessage()方法实现如下:

    private void sendMessage() {
        // 响应发送按钮
        String name = tfName.getText();
        String msg = tfMessage.getText();
        // 检查参数合法性
        if (name == null || msg == null || "".equals(name) || "".equals(msg)) {
            return;
        }
        // 发送消息
        networkService.sendMessage(name, msg);
    }

2.3.2.3

当按下回车键时,如果聊天输入框中有内容,就将其发送出去。
找到keyPressed()方法,改下如下:

    @Override
    public void keyPressed(KeyEvent e) {
        // TODO Auto-generated method stub
        if (e.getKeyCode() == KeyEvent.VK_ENTER) {
            // 发送聊天消息
            sendMessage();
        }
    }

三 测试聊天室系统

3.1 运行服务器端程序

从以下链接下载已编写好的聊天室服务器端代码工程:
链接: https://pan.baidu.com/s/1VlgKoIr-JrHdLXVEieEXdQ 提取码: 5xsm

下载后解压。在eclipse中选择菜单项“File -> Import”,在弹出的对话框中选择“General -> Existing Project into Workspace”,点击“Next”按钮进入下一步对话框,在“Select Root Directory”项下选择刚才解压出来的目录:
在这里插入图片描述

按照之前运行客户端程序同样的方法运行此项目,将会看到如下的窗口:
在这里插入图片描述

3.2 运行客户端程序并连接服务器

连续运行两次我们编写的客户端程序,得到两个客户端程序窗口。分别点击各自的“连接按钮”,观察服务器程序窗口的变化。

分别在各个客户端窗口输入并发送消息,观察是否能够出现在另一客户端窗口。
在这里插入图片描述

注:若下载代码出现中文乱码现象,可以右键类,选择properties

将编码格式设置为UTF-8
在这里插入图片描述

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
一共包括10个以上的聊天程序版本!绝对物有所值! 为感谢大家长期的支持,我将下载所需的资源分下调为2。网络聊天程序设计(可选)  实验要求 1、分析典型网络聊天应用软件(如QQ、MSN等)的实现原理,模拟设计一套网络聊天应用程序,必须实现以下功能: ①按照C/S结构分别设计服务端程序和客户端程序; ②服务端通过图形用户界面实现对服务器的控制,负责维护用户帐户和用户群,并维护用户信息、维持客户端之间的端对端通信和群聊通信、适时维护用户在线信息,并能够发送广播消息。 2、增加尽可能多的功能,用户界面友好,操作简便,代码设计遵从程序设计规范,易读性强,对关键过程和代码进行标注说明。 3、程序设计过程遵从软件工程规范,有需求分析、系统设计和详细设计过程,有相应的规范化说明文档。  实验提示 1、客户端之间的通信是通过服务器进行转发的,对于两个客户端服务器需要创建两个套接字分别维持与客户端之间的连接。当客户端需要向另一个客户发送消息时,它首先将消息发送到服务器,由服务器根据目标用户帐户转发到目标主机。 2、群聊是采用多播技术实现的,也可以采用单播技术实现,但是服务器开销会增加。具体说来,若采用组播技术,当服务端收到来自一个客户端的消息后,向预先分配的该组组播地址转发该消息。若采用单播技术,服务端需要向该组内的所有客户端一一转发该消息。 3、广播消息通过广播方式发送由服务端创建的消息。 4、服务端根据客户的连接和断开情况,实时向其它客户端发送用户在线信息。 实验题目二:自选网络通信程序设计(可选)  实验要求 可以自选与网络通信相关的设计题目,要求如下: 1、在确定实验题目、设计内容以及设计功能指标要求后,向实验指导教师提交书面申请,由实验指导教师根据所选实验题目的难度和工作量确定立题后方能开始实验。 2、选择的实验题目必须具有一定综合性,并能够利用网络通信原理加以解决,同时需要具备一定的工作量。 3、设计的结果要求用户界面友好,操作简便,代码设计遵从程序设计规范,易读性强,对关键过程和代码进行标注说明。 4、程序设计过程遵从软件工程规范,有需求分析、系统设计和详细设计过程,有相应的规范化说明文档。 5、严禁抄袭别人成果,但可以部分借鉴。
以下是一个简单的 Windows 环境下使用 socket 进行客户端服务器通信的 C 代码示例。 服务器端代码: ```c #include <stdio.h> #include <winsock2.h> #pragma comment(lib, "ws2_32.lib") // 链接 ws2_32.lib 库 int main() { WSADATA wsaData; SOCKET serverSocket, clientSocket; SOCKADDR_IN serverAddr, clientAddr; int clientAddrLen = sizeof(clientAddr); char buffer[1024] = {0}; // 初始化 Winsock int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData); if (iResult != 0) { printf("WSAStartup failed: %d\n", iResult); return 1; } // 创建套接字 serverSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (serverSocket == INVALID_SOCKET) { printf("socket failed: %ld\n", WSAGetLastError()); WSACleanup(); return 1; } // 绑定套接字到本地端口 serverAddr.sin_family = AF_INET; serverAddr.sin_addr.s_addr = htonl(INADDR_ANY); serverAddr.sin_port = htons(8888); if (bind(serverSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) { printf("bind failed: %d\n", WSAGetLastError()); closesocket(serverSocket); WSACleanup(); return 1; } // 监听客户端连接请求 if (listen(serverSocket, 5) == SOCKET_ERROR) { printf("listen failed: %d\n", WSAGetLastError()); closesocket(serverSocket); WSACleanup(); return 1; } // 等待客户端连接 printf("等待客户端连接...\n"); clientSocket = accept(serverSocket, (SOCKADDR*)&clientAddr, &clientAddrLen); if (clientSocket == INVALID_SOCKET) { printf("accept failed: %d\n", WSAGetLastError()); closesocket(serverSocket); WSACleanup(); return 1; } // 接收客户端消息并回复 while (1) { int bytesReceived = recv(clientSocket, buffer, sizeof(buffer), 0); if (bytesReceived > 0) { printf("接收到客户端消息: %s\n", buffer); send(clientSocket, buffer, bytesReceived, 0); } } // 关闭套接字 closesocket(serverSocket); WSACleanup(); return 0; } ``` 客户端代码: ```c #include <stdio.h> #include <winsock2.h> #pragma comment(lib, "ws2_32.lib") // 链接 ws2_32.lib 库 int main() { WSADATA wsaData; SOCKET clientSocket; SOCKADDR_IN serverAddr; char buffer[1024] = {0}; // 初始化 Winsock int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData); if (iResult != 0) { printf("WSAStartup failed: %d\n", iResult); return 1; } // 创建套接字 clientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (clientSocket == INVALID_SOCKET) { printf("socket failed: %ld\n", WSAGetLastError()); WSACleanup(); return 1; } // 连接到服务器 serverAddr.sin_family = AF_INET; serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); serverAddr.sin_port = htons(8888); if (connect(clientSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) { printf("connect failed: %d\n", WSAGetLastError()); closesocket(clientSocket); WSACleanup(); return 1; } // 发送消息并等待回复 while (1) { printf("请输入消息:"); gets(buffer); send(clientSocket, buffer, strlen(buffer), 0); int bytesReceived = recv(clientSocket, buffer, sizeof(buffer), 0); if (bytesReceived > 0) { buffer[bytesReceived] = '\0'; printf("收到服务器回复:%s\n", buffer); } } // 关闭套接字 closesocket(clientSocket); WSACleanup(); return 0; } ``` 注意:以上代码仅供参考,实际应用中需要进行错误处理和安全性校验。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值