这次在java实验的时候,要求使用server socket编写服务器和客户端的网络通信。最开始认为应该是挺简单的,但是后来发现低估了它。出现了不少的问题,所以也在这里与大家分享。

问题描述

服务器程序的处理规则如下:
1) 向客户端程序发送Verifying Server!。
2) 若读口令次数超过3次,则发送Illegal User!给客户端,程序退出。否则向下执行步骤3)。
3) 读取客户端程序提供的口令。
4) 若口令不正确,则发送PassWord Wrong!给客户端,并转步骤2),否则向下执行步骤5)。
5) 发送Registration Successful!给客户端程序。

客户端程序的处理规则如下:
1) 读取服务器反馈信息。
2) 若反馈信息不是Verifying Server!,则提示Server Wrong!,程序退出。否则向下执行步骤3)
3) 提示输入PassWord并将输入的口令发送给服务器。
4) 读取服务器反馈信息。
5) 若反馈信息是Illegal User!,则提示Illegal User!,程序退出。否则向下执行步骤6)
6) 若反馈信息是PassWord Wrong!,则提示PassWord Wrong!,并转步骤3),否则向下执行步骤。
7) 输出Registration Successful!。

初步实现

首先,我们一定要清楚,这次和之前的程序不同,虽然都是在本地上,但是服务器客户端需要两个启动程序来实现,以达到我们模拟远程连接的效果。

然后就是如何利用socket实现我们的功能了。

clipboard.png

通过上面的图示,我们可以知道,首先需要先开启服务器,然后等待客户端的连接。

当客户端通过socket进行连接后,服务器端也会建立一个socket对象来帮助实现服务器和客户端的通信。

这时候就建立了一个TCP连接,我们就可以在服务器写数据,然后在客户端读取了,实现双方通信。

最后,当有一方决定通信结束,就会关闭连接。通信结束。

下面是初步实现的代码:

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * 服务器
 */
public class ServerTest {
    public static void main(String[] args) {
        try {
            ServerSocket serverSocket = new ServerSocket(8080);
            Socket clientSocket = serverSocket.accept();

            String welcome  = "verifying server!";
            OutputStream outputStream = clientSocket.getOutputStream();
            outputStream.write(welcome.getBytes());

            InputStream inputStream = clientSocket.getInputStream();
            int time = 0;

            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
            String password = bufferedReader.readLine();

            // 获取登录信息,允许3次登录
            while (time < 3) {
                if (password.equals("123")) {
                    outputStream.flush();
                    outputStream.write("Registration Successful!".getBytes());
                    break;
                } else {
                    outputStream.write("PassWord Wrong!".getBytes());
                    outputStream.flush();
                    password = bufferedReader.readLine();
                    time++;
                }
            }

            if (time >= 3) {
                outputStream.flush();
                outputStream.write("Illegal User!".getBytes());
            }

            outputStream.close();
            clientSocket.close();
            serverSocket.close();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
import java.io.*;
import java.net.Socket;

/**
 * 客户端
 */
public class ClientTest {
    public static void main(String[] args) {
        try {
            Socket socket = new Socket("127.0.0.1", 8080);

            String aline = new String();
            
            // 获取输入流
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            aline = bufferedReader.readLine();

            // 访问失败
            if (!aline.equals("verifying server!")) {
                System.out.println("Server Wrong!");
                return;
            }

            // 获取响应结果
            String feedback = bufferedReader.readLine();

            while (feedback == null || feedback.equals("PassWord Wrong!")) {
                if (feedback != null) {
                    if (feedback.equals("PassWord Wrong!")) {
                        System.out.println("PassWord Wrong!");
                    } else if (feedback.equals("Registration Successful!")) {
                        System.out.println("Registration Successful!");
                        break;
                    }
                }

                System.out.println("输入密码:");
                
                // 输入密码
                Scanner scanner = new Scanner(System.in);
                OutputStream outputStream = socket.getOutputStream();
                String password = scanner.nextLine();
                outputStream.write(password.getBytes());
                
                feedback = bufferedReader.readLine();
            }

            // 关闭连接
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

初步实现后,运行:一片空白。什么都没有发生。

出问题了很正常,断点调试一下吧。发现问题所在:

clipboard.png

客户端执行到这一行时,停止,然后服务器端接着执行;

clipboard.png

同样的服务器端也到这行就停止了。

原因是服务器一方等着输入密码,而客户端一方等着响应结果,然后又没有开始输入密码。所以双方就这么一直等着,谁都不动了。

解决

通过上面的分析,我们只要将僵局打破就能够解决问题。所以就先输入密码,然后再获取响应结果。这样就不会出问题了。

所以服务器端的代码并不需要做什么改动,只用修改客户端程序就行了。

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

/**
 * 客户端
 */
public class ClientTest {
    public static void main(String[] args) {
        try {
            Socket socket = new Socket("127.0.0.1", 8080);

            String aline = new String();
            
            // 获取输入流
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            aline = bufferedReader.readLine();

            // 访问失败
            if (!aline.equals("verifying server!")) {
                System.out.println("Server Wrong!");
                return;
            }

            // 主要修改这里,不先获取响应结果
            // 初始化响应结果
            String feedback = null;

            while (true) {
                if (feedback != null) {
                    if (feedback.equals("PassWord Wrong!")) {
                        System.out.println("PassWord Wrong!");
                    } else if (feedback.equals("Registration Successful!")) {
                        System.out.println("Registration Successful!");
                        break;
                    }
                }

                System.out.println("输入密码:");
                
                // 输入密码
                Scanner scanner = new Scanner(System.in);
                OutputStream outputStream = socket.getOutputStream();
                String password = scanner.nextLine();
                outputStream.write(password.getBytes());
                
                feedback = bufferedReader.readLine();
            }

            // 关闭连接
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

clipboard.png

成功实现!


升级:利用多线程实现

功能成功实现之后,还不能满足于当前的状况。由于在实际使用socket的时候,很多时候都不是简单的一对一的情况,这种情况下,就需要使用多线程来帮助我们了。

使用多线程,我们可以实现一个服务器多个客户端的情况,每当有一个客户端请求连接,我们就会建立一个线程。

1.服务器端线程实现

首先将服务器独立成一个线程:

/**
 * 服务器线程
 */
public class ServerThread extends Thread {
    private ServerSocket serverSocket;
    private Socket clientSocket;

    public ServerThread(int port) {
        try {
            serverSocket = new ServerSocket(port);
            // 接受客户端连接请求
            clientSocket = serverSocket.accept();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在构造函数中我们初始化服务器的socket,然后等待客户端的连接。接着就是最主要的部分了,线程主要执行的逻辑功能:run

@Override
public void run() {
    try {
        // 提示连接成功
        DataOutputStream dataOutputStream = new DataOutputStream(clientSocket.getOutputStream());
        dataOutputStream.writeUTF("verifying server!");

        // 获取输入流
        InputStream inputStream = clientSocket.getInputStream();
        DataInputStream dataInputStream = new DataInputStream(inputStream);
        String password = dataInputStream.readUTF();

        // 获取登录信息,允许3次登录
        int time = 0;

        // 密码校验,允许输入3次
        while (time < 3) {
            if (password.equals("123")) {
                dataOutputStream.writeUTF("Registration Successful!");
                break;
            } else {
                dataOutputStream.writeUTF("PassWord Wrong!");
                password = dataInputStream.readUTF();
                time++;
            }
        }

        if (time >= 3) {
            dataOutputStream.writeUTF("Illegal User!");
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

run没有什么可以说的,主要就是实现服务器与客户端交互的时候执行的功能。然后在执行线程的时候,会自动调用run方法。

最后就是启动服务器端了:

/**
 * 服务器端
 */
public class ServiceTest {
    public static void main(String[] args) {
        ServerThread serverThread = new ServerThread(8080);
        serverThread.start();
    }
}

同样,启用8080端口。

2.客户端线程实现

类似服务器端,首先先建立客户端线程:

/**
 * 客户端线程
 */
public class ClientThread extends Thread {
    private Socket socket;

    public ClientThread(String host, int port) {
        try {
            this.socket = new Socket(host, port);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在构造函数中,建立客户端的socket对象。然后实现run方法。

@Override
public void run() {
    try {
        // 获取输入流
        DataInputStream dataInputStream = new DataInputStream(socket.getInputStream());
        String aline = dataInputStream.readUTF();

        // 连接服务器失败
        if (!aline.equals("verifying server!")) {
            System.out.println("Server Wrong!");
            return;
        }

        // 获取响应结果
        String feedback = null;

        // 进行密码输入
        while (true) {
            if (feedback != null) {
                if (feedback.equals("PassWord Wrong!")) {
                    System.out.println("PassWord Wrong!");
                } else if (feedback.equals("Registration Successful!")) {
                    System.out.println("Registration Successful!");
                    System.exit(1);
                }
            }

            System.out.println("输入密码:");
            Scanner scanner = new Scanner(System.in);
            DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());
            String password = scanner.nextLine();
            dataOutputStream.writeUTF(password);
            
            // 获取响应结果
            feedback = dataInputStream.readUTF();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

run方法中,主要实现了输入密码的功能。

最后就是启动客户端线程:

 /**
 * 客户端
 */
public class ClientTest {
    public static void main(String[] args) {
        ClientThread clientThread = new ClientThread("127.0.0.1", 8080);
        clientThread.start();
    }
}

总结

这次的实验给我提了个醒,看似简单的东西,也永远不要轻视它。只有拿出应有的实例去解决问题,问题才是你眼中那个简单的问题。


喵先生的进阶之路
348 声望21 粉丝