Socket网络编程

网络编程

Socket

IP和端口号

什么是IP
IP是根据TCP/IP协议划定,由32位二进制数组成,而且在因特网上是唯一的数值

例如:某台计算机,连上网的IP是:

11010101 01001001 11110000 11001100

为了便于记忆,会将这32位二进制数,每8位一组,每段之间用小数点分割

11010101.01001001.11110000.11001100

再将每八位转化为十进制

213.73.240.204

如何查看自己电脑的ip

  1. 按win+R, 输入cmd,打开dos窗口

  2. 在dos窗口输入ipconfig

什么是端口号

端口号(port number)就是计算机为了给每个网络程序分配一个独一无二的区别符,有了这些端口号,就可以准确定位到具体的程序。

端口号是个整数,范围0-65535,分为周知端口和动态端口

  1. 周知端口就是众所周知的端口,是端口号中的明星,本身的存在就是有自身用途,这些端口我们一般不使用,范围是0-1023。

  2. 动态端口,剩下的端口号都是动态端口,动态端口的意思就是将这些端口动态的分配给每个需要端口号的程序,当开启一个程序时,就分配给它一个端口

Socket概述

在计算机领域中,Socket也被称为套接字编程,它是计算机之间进行通信的一种约定或者说是一种方式。

应用程序可以通过它发送或者接收数据,可以对其发送过来的内容像处理文件一样,打开、关闭或者读写等操作,套接字允许应用程序将I/O插入到互联网上,并与网络中的其他程序进行通信。

Socket常用方法

服务器端ServerSocket

在服务器端选择一个端口号,然后在指定的端口号上等待客户端发起的连接

构造方法:

ServerSocket(int port) 创建一个绑定特定端口号的服务器套接字
accept() 侦听并接受到发送到此套接字的连接
close() 关闭此套接

客户端Socket

构造方法:

Socket(String host,int port) 创建一个套接字,并且连接到host,并且绑定端口号
close() 关闭此套接字
getInputStream() 返回此套接字的输入流
getOutputStram() 返回此套接字的输出流

聊天室

image

服务器端代码

package socket;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Scanner;

/**
* 聊天室的服务器端
*/
public class Server {
   /*
    * java.net.ServerSocket
    * ServerSocket是运行在服务器端的,它的主要工作:
    * 1:打开服务器端口(客户端就是根据这个端口与服务器链接)
    * 2:监听该服务器端口,一旦一个客户端链接,就会返回一个Socket实例,
    * 并通过这个Socket实例与链接的客户端进行交互
    * 如果我们将Socket比喻为"电话",ServerSocket比喻为"总机"
    */
   private ServerSocket server;
   //该数组用于存放所有客户端的输出流,用于广播消息给所有的客户端
   //private PrintWriter[] allOut = {};
   private Collection<PrintWriter> allOut = new ArrayList<>();
   public Server() {
       try {
           System.out.println("正在启动服务器...");
           server = new ServerSocket(8088);
           System.out.println("服务器启动完毕!!!");
      } catch (IOException e) {
           e.printStackTrace();
      }
  }

   public void start() {
       try {
           while (true) {
               System.out.println("等待客户端链接...");
               /*
                * ServerSocket中提供了一个方法:
                * Socket accept()
                * 当程序执行到这个方法后,会阻塞,当有一个客户端访问时,
                * 这个方法就会立即返回一个Socket对象,通过这个Scoket与该客户端进行链接
                */
               Socket socket = server.accept();
               System.out.println("一个客户端链接了!!!");
               ClientHandler handler = new ClientHandler(socket);
               Thread t = new Thread(handler);
               t.start();
          }
      } catch (IOException e) {
           e.printStackTrace();
      }
  }

   public static void main(String[] args) {
       Server server = new Server();
       server.start();
  }

   /*
    * 该线程负责与特定客户端交互
    * 内部类:可以访问外部类的任意属性
    * */
   private class ClientHandler implements Runnable {
       private Socket socket;

       //声明含参构造,负责实例化对象时,接收socket参数
       public ClientHandler(Socket socket) {
           this.socket = socket;
      }

       @Override
       public void run() {
           PrintWriter pw = null;
           try {
               /*
                * 通过Secket的方法:
                * InputStream getInputStream()
                * 可以获取一个输入字节流,可以读取来自远端计算机发送过来的字节数据
                */
               InputStream in = socket.getInputStream();
               InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
               BufferedReader br = new BufferedReader(isr);
               //通过socket获取输出流,用于给客户端发送信息
               OutputStream out = socket.getOutputStream();
               //创建转换流,链接字节流和字符流
               OutputStreamWriter osw =
                       new OutputStreamWriter(out, StandardCharsets.UTF_8);
               //创建缓冲字符流,提高写出效率
               BufferedWriter bw = new BufferedWriter(osw);
               //按行写入字符串,自带行刷新
               pw = new PrintWriter(bw, true);
               //将该客户端的输出流pw存储到allOut数组中
               //由于数组的长度一旦创建不可改变,所以输出流数量不可知,所以每存储一个,需要进行数组扩容
               //this不行,因为this是ClientHandler.
               /*
                * 一般情况下,同步监视器对象选取时,就选择多个线程并发操作的临界资源就可以了,allOut数组进行了扩容了,数组一旦扩容,数组就变成一个新数组了,内存地址也发生了变化,所以对于多线程而言,此时allOut已经不是唯一的,所以同步代码块失效
                * */
               synchronized (Server.this) {//内部类指向外部类对象
                   //1.扩容数组,让allOut数组的长度+1
                   //allOut = Arrays.copyOf(allOut, allOut.length + 1);
                   //2.将pw存入到共享的数组中(存储到数组中的最后一个位置)
                   //allOut[allOut.length - 1] = pw;
                   //集合不需要扩容,所以直接添加即可
                   allOut.add(pw);
              }
               //广播通知所有的客户端有新的客户端上线了
               sendMessage("有新的客户端上线了!当前在线人数:" + allOut.size());
               String line;
               while ((line = br.readLine()) != null) {//当读取的内容是null,就停止读取
                   System.out.println("客户端说:" + line);
                   //将消息回复给所有客户端
                   sendMessage(line);//这个方法等效于下面注释的代码
                   //allOut.fori
//                   for (int i = 0; i < allOut.length; i++) {
//                       allOut[i].println(line);
//                   }
              }
          } catch (IOException e) {
               e.printStackTrace();
          } finally {//不论客户端是正常断开还是异常断开,都只要执行取出输出流操作
               synchronized (Server.this) {
                   //遍历allOut数组 allOut.fori
//                   for (int i = 0; i < allOut.length; i++) {
//                       //找到要删除的元素(利用内存地址是否相同判断)
//                       if (allOut[i] == pw) {
//                           //将最后一个元素替换到目标删除元素
//                           allOut[i] = allOut[allOut.length - 1];
//                           //将数组进行缩容
//                           allOut = Arrays.copyOf(allOut, allOut.length - 1);
//                           //由于上面的代码执行了,说明已经将下线的客户端的对应服务器的输出流取出了,就不需要再次执行for循环,因为后面的元素一定不是要删除的元素,所以可以直接利用break关键字停止循环,可以有效地提升效率
//                           break;
//                       }
//                   }
                   //会自动通过equals方法遍历集合,找到要删除的元素,进行删除
                   allOut.remove(pw);
              }
               sendMessage("有客户端下线了!当前在线人数:" + allOut.size());
               //服务器端也关闭电话
               try {
                   socket.close();
              } catch (IOException e) {
                   e.printStackTrace();
              }
          }
      }
       /*
        * 广播消息给所有的客户端(可以发客户端给服务器发送的内容,也可以发送在线人数)
        *   send 发送 Message 消息
        * */
       private void sendMessage(String msg) {
           synchronized (Server.this){
//               for (int i = 0; i < allOut.length; i++) {
//                   allOut[i].println(msg);
//               }
               for (PrintWriter pw : allOut) {
                   pw.println(msg);
              }
          }
      }
  }
}

客户端代码

package socket;

import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

/**
* 聊天室客户端
*/
public class Client {
   /*
    * java.net.Socket 套接字,原意是插座
    * Socket封装了TCP协议的通讯细节,我们使用这个工具就可以与远端计算机建立TCP链接,
    * 并基于一堆流的IO操作完成与远端计算机的数据交换
    */
   private Socket socket;

   /*
    * 初始化客户端
    */
   public Client() {
       try {
           System.out.println("正在链接服务器...");
           /*
            * 实例化Socket时,需要传入两个参数:
            * 参数1(String):远端计算机的地址信息(IP)
            *             本机地址IP可以选择如下:
            *                 localhost(域名)
            *                 127.0.0.1
            * 参数2(int):远端计算机打开的服务端口
            *           此端口值必须要和服务器端占用的端口保持一致
            *
            */
           socket = new Socket("localhost", 8088);
      } catch (IOException e) {
           e.printStackTrace();
      }
  }

   /*
    * 客户端开始工作的方法
    */
   public void start() {
       try {
           //启动一个线程来读取服务器端发送的信息
           ServerHandler handler = new ServerHandler();
           Thread t = new Thread(handler);
           //将子线程设置为守护线程,当主线程结束时,也会一同关闭
           t.setDaemon(true);
           t.start();
           /*
            * 通过Socket的方法:
            * OutputStream getOutputStream()
            * 获取的字节输出流写出的字节会通过网络发送给链接的远端计算机
            */
           OutputStream out = socket.getOutputStream();
           OutputStreamWriter osw = new OutputStreamWriter(out, StandardCharsets.UTF_8);
           BufferedWriter bw = new BufferedWriter(osw);
           PrintWriter pw = new PrintWriter(bw, true);
           Scanner scanner = new Scanner(System.in);//创建控制台扫描器
           while (true) {//一直接收控制输入的内容
               String line = scanner.nextLine();//获取本次在控制输入的内容
               if ("exit".equalsIgnoreCase(line)) {//判断输入的内容是否是exit
                   break;//如果是,就跳出循环
              }
               pw.println(line);//如果输入的不是exit,就将内容发送给服务器
          }
      } catch (IOException e) {
           e.printStackTrace();
      } finally {
           try {
               socket.close();//与远端计算机断开连接,并且进行TCP的挥手,同时关闭连接的流
          } catch (IOException e) {
               e.printStackTrace();
          }
      }
  }

   //项目的主入口
   public static void main(String[] args) {
       Client client = new Client();
       client.start();
  }

   /*
    * 该线程负责读取服务器端发送的消息
    */
   private class ServerHandler implements Runnable {
       @Override
       public void run() {
           //通过socket获取输入流读取服务器发送的消息
           try {//线程的run方法中,不允许使用throws声明异常异常
               InputStream in = socket.getInputStream();
               InputStreamReader isr =
                       new InputStreamReader(in, StandardCharsets.UTF_8);
               BufferedReader br = new BufferedReader(isr);
               //创建流之后,继续循环读取服务器发送的信息
               String line;
               while ((line = br.readLine()) != null) {
                   System.out.println(line);
              }
          } catch (IOException e) {
               //打印异常代码删除,因为只有客户端异常断开时,才会报,所以不需要看
          }

      }
  }
}

测试

切记先运行服务器,后运行客户端!!!

常见问题:

表示端口被占用,解决方式:换个没有被占用的端口

表示连接被拒绝,因为服务器没启动,解决方式:先启动服务器,然后再启动客户端