Socket实现攻略:
一、创建两个类
?1、用户使用的,名为:Client
①定义私有成员属性:Socket socket;
java.net.Socket:套接字 Socket封装了TCP协议的通讯细节,使得我们使用它可以与服务端建立网络连接,并通过它获取两个流(一个输入流,一个输出流),然后使用这两个流的读写操作完成与服务端的数据交互。
②定义无参构造函数:
在构造函数中实例化对象,socket = new Socket(参数1,参数2); 实例化Socket时通常需要传入两个参数: 参数1:服务端的地址信息(IP地址,如果连接本机可以用localhost)。 参数2:服务端打开的服务端口,即服务端ServerSocket申请的端口。
? ? private Socket socket;
? ? public Client(){
? ? ? ? try {
? ? ? ? ? ? System.out.println("正在连接服务器……");
? ? ? ? ? ? socket = new Socket("192.168.43.215",2222);
? ? ? ? ? ? System.out.println("服务器连接成功!"+socket.getInetAddress());
? ? ? ? } catch (IOException e) {
? ? ? ? ? ? e.printStackTrace();
? ? ? ? }
? ? }
?2、服务器的,名为:Server
①定义私有成员属性:ServerSocket serverSocket
运行在服务器端的ServerSocket主要有两个作用: ? ? 1、向系统申请服务端口,客户端的Socket就是通过这个端口与服务器建立连接的。 ? ? 2、监听服务端口,一旦一个客户通过该端口建立连接会自动创建一个Socket,服务端就可以通过这个Socket与客户端交互了。如果我们把Socket比喻为电话,那么ServerSocket相当于是某客服中心的总机。
②定义一个集合,用来存储可能开启的很多个客户端。
private List<PrintWriter> c = Collections.synchronizedList(new ArrayList<>());创建一个线程安全的集合,其中add,remove等方法上都直接使用了synchronized修饰, 锁个线程是不能同事调用一个集合的这些方法的,保证同步执行。
③ 定义无参构造函数:
在构造函数中实例化对象serverSocket = new ServerSocket(222);设置端口号。 实例化时需要指定服务器端口,如果该端口被当前系统其他应用程序占据时,会抛出异常。
?private ServerSocket serverSocket;
private List<PrintWriter> c = Collections.synchronizedList(new ArrayList<>());
? ? public Server() {
? ? ? ? try {
? ? ? ? ? ? System.out.println("正在启动服务端……");
? ? ? ? ? ? serverSocket = new ServerSocket(222);
? ? ? ? ? ? System.out.println("服务端连接成功!");
? ? ? ? } catch (IOException e) {
? ? ? ? ? ? e.printStackTrace();
? ? ? ? }
? ? }
二、服务器端需要做的事:
服务器端需要:接收客户端发送的信息,和将汇总的信息发回给客户端,实现通信功能。
1、实现客户端与服务器的互通:accept
① 不确定客户端数量,所以可以用死循环,保证一直能有新用户连接
② 客户端与服务器连接并创建启动线程,使用ServerSocket提供的方法: [Socket] accept():
这是一个阻塞方法,调用后程序进入阻塞状态,直到一个客户端实例化Socket与当前服务端建立连接,此时accept方法会立即返回一个Socket实例,服务端通过它就可以与客户端交互了。可以理解为这个动作相当于是总机的“接电话”操作。 注意:设置线程这里也要try-catch,否则会报错
? ? ? int i = 1;
? ? ? ? ? ? while (true) {
? ? ? ? ? ? ? ? System.out.println("等待客户端连接……");
? ? ? ? ? ? ? ? Socket socket = serverSocket.accept();
? ? ? ? ? ? ? ? System.out.println("第" + i + "个客户端连接成功!" + socket.getInetAddress());
? ? ? ? ? ? ? ? i++;
? ? ? ? ? ? ? ? //启动一个线程用来与客户端交互:
? ? ? ? ? ? ? ? //1、创建线程任务:
? ? ? ? ? ? ? ? ClientHandler handler = new ClientHandler(socket);
? ? ? ? ? ? ? ? //2、创建一个线程执行该任务
? ? ? ? ? ? ? ? Thread t = new Thread(handler);
? ? ? ? ? ? ? ? //3、启动线程
? ? ? ? ? ? ? ? t.start();
? ? ? ? ? ? }
2、定义一个属于类发送信息的方法,将服务器收到的信息发给客户端
public void sendMessage(String message){
c.forEach(pw -> pw.println(message));
}
3、创建一个线程任务类ClientHandler implements Runnable,用于与指定的客户端进行交互。
每个连接服务器的客户端都是通过线程进行交互的。 即:一个客户端靠一个线程进行交互
① 私有化内部类的属性:private Socket socket
② 有参的构造方法:传递一个socket给当前赋值
private class ClientHandler implements Runnable {
private Socket socket;
private String address;//处理当前线程处理的客户端地址
public ClientHandler(Socket socket) {
this.socket = socket;
//通过socket获取远端计算机地址信息(对于服务器而言,远端就是客户端)
this.address = socket.getInetAddress().getHostAddress();
}
④ 定义线程任务,重写run: Socket提供的方法:[InputStream] getInputStream():
通过该方法获取的字节输入流取的时远端计算机发送过来的数据。这里相当于是读取当前服务端中这个Socket对应的远端(客户端)那边Socket获取的输出流写出的字节数据。
⑤ 使用流完成信息的写入写出:
@Override
public void run() {
PrintWriter pw =null;
try {
pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8")), true);
c.add(pw);
System.out.println(address+"上线了,当前在线人数:"+c.size());
//广播上线通知:
sendMessage(address+"上线了,当前在线人数:"+c.size());
BufferedReader bw = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
String line;
while ((line = bw.readLine()) != null) {
System.out.println(address + "说:" + line);
//将消息回复给所有客户端
sendMessage(address + "说:" + line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//下线时:移除对象并且广播
try {
c.remove(pw);
sendMessage(address+"下线了,当前在线人数:"+c.size());
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
三、客户端要做的事:
1、定义一个start方法,接收用户从控制台输入的信息
① 使用流连接:Socket提供的方法:[OutputStream] getOutputStream()
该方法会获取一个字节输出流,通过这个输出流写出的字节数据会通过网络发送给对方。设定结束词语。
public void start(){
? ? try(OutputStream out = socket.getOutputStream();
? ? PrintWriter pw =new PrintWriter(new BufferedWriter(new OutputStreamWriter(out,"UTF-8")),true);) {
? ? ? ? ?Scanner sc = new Scanner(System.in);
? ? ? ? ?System.out.println("开始聊天吧!单独输入88|拜拜则退出聊天室~");
? ? ? ? ?while (true){
? ? ? ? ? ? ?String line = sc.next();
? ? ? ? ? ? ?if(line.equals("88")|line.equals("拜拜")){
? ? ? ? ? ? ? ? ?System.out.println("本次聊天已结束,感谢使用聊天室~");
? ? ? ? ? ? ? ? ?break;
? ? ? ? ? ? ? }
? ? ? ? ? ? ? pw.println(line);
? ? ? ? ? }
? ? ? ? } catch (IOException e) {
? ? ? ? ? ? e.printStackTrace();
? ? ? ? } finally {
? ? ? ? ? ? try {
? ? ? ? ? ? ? ? socket.close();
? ? ? ? ? ? } catch (IOException e) {
? ? ? ? ? ? ? ? e.printStackTrace();
? ? ? ? ? ? }
? ? ? ? }
? ? }
2、定义线程任务ServerHandler implements Runnable
①设置run方法,Socket提供的方法:[InputStream] getInputStream():
通过该方法获取的字节输入流取的时远端计算机发送过来的数据。 这里相当于是读取当前服务端中这个Socket对应的远端(客户端)那边Socket获取的输出流写出的字节数据。
设置字节输出流,读取其他客户端或者自己发送给服务器后,被服务器返回的数据,如果只是使用输入流,则看不到别人的信息,功能不健全。输入自己说的,读出大家说的,这样可以实现互通。
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();
}
}
|