socket编程(二)---- 使用套接字连接多个客户端

时间:2021-10-13 11:02:16
 

        在(一)中,客户端和服务器之间只有一个通讯线程,所以它们之间只有一条Socket信道。如果我们引入多线程机制,则可以让一个服务器端同时监听并接收多个客户端的请求,并同步地为他们提供通讯服务。基于多线程的通讯方式,将大大地提高服务器端的利用效率,并能使服务器端能具备完善的服务功能。

       下面通过一个例子来加深理解: 

       第一步:写服务端线程类

public class ThreadServer extends Thread {
private static String NAME = "服务器Deny";
private BufferedReader reader;
private PrintWriter writer;
private Socket socket;
private String str;

@Override
public void run() {
try {
while (true) {
str = reader.readLine();
if (str.endsWith("byebye"))
break;
System.out.println("客户端---" + socket + "说:" + str);
writer.println("我是" + NAME + ",客户端说:" + str);
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}

public ThreadServer(Socket s) {
try {
socket = s;
reader = new BufferedReader(new InputStreamReader(
socket.getInputStream()));
writer = new PrintWriter(new OutputStreamWriter(
socket.getOutputStream()), true);
start();
} catch (IOException e) {
e.printStackTrace();
}
}
}


说明

1.这个类通过继承Thread类来实现线程的功能,也就是说,在其中的run方法里,定义了该线程启动后要执行的业务动作。

2.这个类提供了两种类型的重载函数。在参数类型为Socket的构造函数里, 通过参数,初始化了本类里的Socket对象,同时实例化了两类IO对象。在此基础上,通过start方法,启动定义在run方法内的本线程的业务逻辑。

3.在定义线程主体动作的run方法里,通过一个for(;;)类型的循环,根据IO句柄,读取从Socket信道上传输过来的客户端发送的通讯信息。如果得到的信息为“byebye”,则表明本次通讯结束,退出for循环。

4.catch从句将处理在try语句里遇到的IO错误等异常,而在finally从句里,将在通讯结束后关闭客户端的Socket句柄。

上述的线程主体代码将会在服务器端Server类里被调用。

 

第二步:服务端

public class ServerMain {
private static int PORT = 8899;

public static void main(String[] args) throws IOException {
ServerSocket server = null;
Socket socket = null;
server = new ServerSocket(PORT);
System.out.println("server start...");
try {
for (;;) {
socket = server.accept();
System.out.println("client连接上:" + socket.toString());
new ThreadServer(socket);
}

} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
if (server != null)
server.close();
}
}
}

说明:

1.首先定义了通讯所用的端口号。2.在main函数中根据端口号创建一个ServerSocket类型的服务端socket,用来同客户端通讯。3.在for循环中,调用accept方法,监听从客户端请求过来的socket,请注意,这里又是一个阻塞。当客户端有请求过来时,将通过ThreadServer的构造函数,创建一个线程类,用来接收客户端发送过来的字符串。在这里可以再一次观察ThreadServer类,在构造方法中调用start()方法,开启run方法,在run方法中输入输出。4.在finally中关闭socket,结束通讯。


第三步:客户端线程类

public class ThreadClient extends Thread {
private Socket socket;
private static int PROT = 8899;
private int id = ID++;
private static int ID = 0;
private BufferedReader reader;
private PrintWriter writer;

@Override
public void run() {
try {
writer.println("HI server, i am " + id);
String str = reader.readLine();
System.out.println("id " + id + ",come from server" + str);
writer.println("byebye");
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
if (socket != null)
try {
socket.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

}

public ThreadClient(InetAddress address) {
try {
socket = new Socket(address, PROT);
reader = new BufferedReader(new InputStreamReader(
socket.getInputStream()));
writer = new PrintWriter(new OutputStreamWriter(
socket.getOutputStream()), true);

start();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}


说明:

1. 在构造函数里, 通过参数类型为InetAddress类型参数和端口号,初始化了本类里的Socket对象,随后实例化了两类IO对象,并通过start方法,启动定义在run方法内的本线程的业务逻辑。

2. 在定义线程主体动作的run方法里,通过IO句柄,向Socket信道上传输本客户端的ID号,发送完毕后,传输”byebye”字符串,向服务器端表示本线程的通讯结束。

3.同样地,catch从句将处理在try语句里遇到的IO错误等异常,而在finally从句里,将在通讯结束后关闭客户端的Socket句柄。

第四步:客户端启动

public class ClientMain {
public static void main(String[] args) {
int num;
try {
InetAddress address = InetAddress.getByName("localhost");
for (num = 0; num < 3; num++) {
new ThreadClient(address);
}
} catch (UnknownHostException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}


说明:

       通过for循环,根据指定的待创建的线程数量,通过ThreadClient构造函数,创建若干个客户端线程,同步地和服务器端通讯。
这段代码执行以后,在客户端将会有3个通讯线程,每个线程首先将先向服务器端发送信息,然后发送”byebye”,终止该线程的通讯。

执行测试:

第一步,我们先要启动服务器端的服务器端代码,启动后,在控制台里会出现如下的提示信息:

server start...

第二步,我们在启动完服务器后,运行客户端代码,运行后,我们观察服务器端的控制台,会出现如下的信息:

client连接上:Socket[addr=/127.0.0.1,port=11138,localport=8899]
client连接上:Socket[addr=/127.0.0.1,port=11139,localport=8899]
客户端---Socket[addr=/127.0.0.1,port=11138,localport=8899]说:HI server, i am 0
客户端---Socket[addr=/127.0.0.1,port=11139,localport=8899]说:HI server, i am 1
client连接上:Socket[addr=/127.0.0.1,port=11140,localport=8899]
客户端---Socket[addr=/127.0.0.1,port=11140,localport=8899]说:HI server, i am 2


这里,请大家注意,由于线程运行的不确定性,从第二行开始的打印输出语句的次序是不确定的。但是,不论输出语句的次序如何变化,我们都可以从中看到,客户端有三个线程请求过来,并且,服务器端在处理完请求后,会关闭Socker和IO。

 

第三步,当我们运行完Client.java的代码后,并切换到Client.java的控制台,我们可以看到如下的输出:

id 0,come from server我是服务器Deny,客户端说:HI server, i am 0
id 1,come from server我是服务器Deny,客户端说:HI server, i am 1
id 2,come from server我是服务器Deny,客户端说:HI server, i am 2
这说明在客户端开启了3个线程,并利用这3个线程,向服务器端发送字符串。

 

而在服务器端,用accept方法分别监听到了这3个线程,并与之对应地也开了3个线程与之通讯。