参考本文时最好已了解Socket基础知识,Socket基础知识与简单案例请参考:。
第一例:Socket实现多个客户端向服务器端通信
实现多个客户端向服务器端的通信首先需要启动一个服务器端用来监听客户端的连接,然后会将连接放入线程中,这时客户端想服务器端发送信息就可以接收到了。为了简化代码,提高可读性,接下来的例子我将不再进行资源的关闭回收。
每当有客户端向服务器请求连接时,服务器会获取客户端的Socket,然后将这个Socket放入一个新的线程中,在线程中监听客户端发送的信息,这时客户端向服务器端发送信息就可以接收到了。
SocketService(服务器端)
package socket.service;
import java.net.ServerSocket;
import java.net.Socket;
public class SocketService {
public static void main(String args[])throws Exception {
ServerSocket serverSocket = new ServerSocket(5208);
System.out.println("服务器启动成功");
while (true) {
Socket socket= serverSocket.accept();
System.out.println("上线通知: " + socket.getInetAddress() + ":" +socket.getPort());
new Thread(new ServerThread(socket)).start();
}
}
}
服务器端线程)
package socket.service;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
public class ServerThread implements Runnable {
public Socket socket;
public ServerThread (Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
while (true) {
String str = br.readLine();
System.out.println(str);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
SocketClient1(第一个客户端)
package socket.service;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class SocketClient1 {
public static void main(String args[])throws Exception{
Socket socket = new Socket("192.168.10.2", 5208);
System.out.println("小一连接成功");
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
PrintWriter pw = new PrintWriter(socket.getOutputStream());
while(true){
pw.println("小一说:"+br.readLine());
pw.flush();
}
}
}
SocketClient2(第二个客户端)
package socket.service;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class SocketClient2 {
public static void main(String args[])throws Exception{
Socket socket = new Socket("192.168.10.2", 5208);
System.out.println("小二连接成功");
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
PrintWriter pw = new PrintWriter(socket.getOutputStream());
while(true){
pw.println("小二说:"+br.readLine());
pw.flush();
}
}
}
这个例子是多个客户端向服务器发送信息,接下来我们分析一下上面的代码逻辑。
首先我们先启动服务器,服务器启动后,进入无限循环中的accept方法,这里的无限循环是为了不断的获取n个客户端连接。在这里会阻塞等待一个socket连接,服务器的代码走到这就停了。
然后我们启动第一个客户端。客户端启动后,服务器会往下走,服务器会新启动一个自己写的线程(ServerThread),将socket连接作为构造函数的参数传入这个自己写的线程中,然后在线程中通过readline方法,阻塞等待客户端socket中输入流的出现,而同时,客户端也会被循环中的readline方法阻塞,等待控制台中的输入流的出现。
这就是一个客户端的连接,这时整个项目就进入等待中,服务器被accept阻塞等待下一个客户端连接,服务器线程被readline阻塞等待socket中输入流的出现,客户端被readline阻塞等待控制台中输入流的出现。
仔细思考后你会发现,这里的服务器端的功能其实只是实例化出一个个服务器线程,而真正进行通信的是服务器线程和客户端,所以这里的服务器线程其实就是上一篇博客中的服务器端,而这里的服务器端只是服务器的工厂类,用来生产出一个个服务器的。
之后同样启动第二个客户端,逻辑同第一个客户端连接。然后我们在第一个客户端的控制台中输入一串字符,这时第一个客户端中的readline就会立刻读取到这段输入流,然后客户端通过PrintWriter将该输入流通过socket的输出流推送到服务器线程的socket中,然后进入下一个循环的等待。
而服务器线程中的readline就会立刻读取到socket的输入流,然后打印到服务器的控制台,之后又会进入下一个循环的等待。在这里其实也可以进行服务器线程向客户端推送消息的操作,但为了简化代码逻辑,这里没有进行更多的操作,在下一个例子中有详细的解释。
然后你在第二个客户端的控制台中输入一串字符,也同样能在服务器端进行打印输出。其实也就是两个客户端可以同时和服务器端的两个线程进行通信,一个服务器端线程阻塞时,不会影响到另一个,所以在服务器端是能够同时看到两个客户端的通信内容的。
这里有几个个线程的注意事项。
第一是自己写线程时一般有两种方式,继承Thread类和实现Runnable接口,一般情况下,如果我们只想重写run方法,都是使用实现Runnable接口的方式,Thread类的底层代码其实也是实现了Runnable接口。
第二是启动线程一般也有两种方式,一个是调用run方法,一个是调用start方法,调用run方法其实并没有启动一个新的线程,而是只有一个线程执行了一下run方法。但start是新建了一个线程,所以线程的启动都是使用start方法。
但实现runnable接口的线程没有start方法,所以需要将实现runnable接口的类封装到Thread类中,然后调用Thread的start方法,如:new Thread(new ServerThread(socket)).start();
理解了逻辑后不难看出,在这个例子中体现的最重要的一点是利用while(true)循环来实例化多个线程,然后使用一个新的线程来单独跑一个服务器,使得多个客户端能够和多个不同的服务器线程通信,形成了多个客户端同时和一个服务器端通信的效果,如果去掉了循环和线程,那么就只能实例化一个服务器,也就无法让多个socket客户端连接,其实也就是上一篇博客中的第一个例子。
第二例:Socket实现客户端与客户端通信
在上一个例子中,是通过实例化多个服务器线程来和客户端连接,从而实现一个服务器连接多客户端的功能。那么如果我们在服务器线程中获取了客户端推送过来的信息后,再将这个信息推送到所有客户端,是不是就实现了客户端与客户端的通信呢?
逻辑是没有错的,但是有两个难点,第一,如何将信息推送到所有客户端呢?所以这就得在服务器端用一个列表来存储所有客户端。第二,上一篇博客的学习中,我们知道获取控制台的信息需要用到readline这个阻塞函数,获取客户端推送过来的信息也需要用到readline这个阻塞函数,如果在一个方法中同时需要用到这两个功能的话,那么就每接收一次消息就需要发送一次消息,不然程序就会阻塞在一个地方。所以每个客户端都应实例化两个线程,一个用来等待控制台输入,一个用来等待服务器推送的消息。
SocketService(服务器端)
package socket.chat;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
public class SocketService {
public static List<Socket> socketList = new ArrayList<Socket>();
public static void main(String args[])throws Exception {
ServerSocket serverSocket = new ServerSocket(5208);
System.out.println("聊天室开启");
while (true) {
Socket socket= serverSocket.accept(); //从连接请求队列中取出一个连接
System.out.println("上线通知: 用户" + socket.getPort()+"上线啦!");
socketList.add(socket);
new Thread(new ServerThread(socket)).start();
}
}
}
ServerThread(服务器端线程)
package socket.chat;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class ServerThread implements Runnable {
public Socket socket;
public ServerThread (Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
while (true) {
String str = br.readLine();
for (Socket item : SocketService.socketList) {
PrintWriter pw = new PrintWriter(item.getOutputStream());
pw.println("用户"+socket.getPort()+"说:"+str);
pw.flush();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
SocketClient1(第一个客户端)
package socket.chat;
import java.net.Socket;
public class SocketClient1 {
public static void main(String args[])throws Exception{
Socket socket = new Socket("192.168.10.2", 5208);
System.out.println("恭喜你连接成功!");
new Thread(new SocketThread1(socket)).start();
new Thread(new SocketThread2(socket)).start();
}
}
SocketClient2(第二个客户端)
package socket.chat;
import java.net.Socket;
public class SocketClient2 {
public static void main(String args[])throws Exception{
Socket socket = new Socket("192.168.10.2", 5208);
System.out.println("恭喜你连接成功!");
new Thread(new SocketThread1(socket)).start();
new Thread(new SocketThread2(socket)).start();
}
}
SocketThread1(监听控制台客户端线程)
package socket.chat;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class SocketThread1 implements Runnable {
public Socket socket;
public SocketThread1(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
PrintWriter pw = new PrintWriter(socket.getOutputStream());
while(true){
String str = br.readLine();
pw.println(str);
pw.flush();
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
SocketThread2(监听服务器客户端线程)
package socket.chat;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.Socket;
public class SocketThread2 implements Runnable {
public Socket socket;
public SocketThread2(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
while(true){
String str = br.readLine();
System.out.println(str);
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
这个例子是客户端与客户端之间的通信,接下来我们分析一下上面的代码逻辑。
首先在服务器端定义一个静态的List用来存放所有的客户端socket。我们首先会启动服务器,服务器启动后,进入无限循环中的accept方法,这里的无限循环是为了不断的获取n个客户端连接。在这里会阻塞等待一个socket连接,服务器的代码走到这就停了。
然后我们启动第一个客户端。客户端启动后,服务器会往下走,首先先将socket连接加入静态列表中,以便服务器线程调用。然后服务器会新启动一个自己写的线程(ServerThread),将socket连接作为构造函数的参数传入这个自己写的线程中,然后在线程中通过readline方法,阻塞等待客户端socket中输入流的出现。
这时,客户端也会将socket作为构造函数的参数来新建两个客户端线程,第一个线程的作用是等待控制台中输入流的出现,被循环中的readline方法阻塞,等待控制台中的输入流的出现;第二个线程的作用是等待服务器端推送过来的socket中输入流的出现。
这就是一个客户端的连接,这时整个项目就进入等待中,服务器被accept阻塞等待下一个客户端连接,服务器线程被readline阻塞等待客户端传过来的socket中输入流的出现,客户端的第一个线程被readline阻塞等待控制台中输入流的出现,客户端的第二个线程被readline阻塞等待服务器传过来的socket中输入流的出现。
之后同样启动第二个客户端,逻辑同第一个客户端连接。然后我们在第一个客户端的控制台中输入一串字符,这时第一个客户端的第一个线程中的readline就会立刻读取到这段输入流,然后该线程通过PrintWriter将该输入流通过socket的输出流推送到服务器线程的socket中,然后进入下一个循环的等待。
而服务器线程中的readline就会立刻读取到客户端传过来的socket的输入流,然后通过服务器端的静态列表获取所有的socket连接,再通过每个socket的输出流将这串字符推送到所有客户端,之后又会进入下一个循环的等待。
这是所有客户端的第二个线程中的readline就会立刻读取到服务器端传过来的输入流,然后将这串字符串打印输出到控制台,然后进入下一个循环的等待。
之后你在第二个客户端的控制台中输入一串字符,也是同样的逻辑。上一个例子是多个客户端向服务器的通信,这个例子加了一步服务器接收到通信后再次向所有客户端发送这次通信内容。上一个例子也提到了的服务器可以向客户端通信,这个例子通过静态List和客户端分两个线程的方式解决了两个难点,自然而然就实现了聊天室功能。
总结一下,本文实现聊天室主要依靠的就是两个技术点:Socket + 线程。