前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >第11次文章:网络编程——聊天室构建

第11次文章:网络编程——聊天室构建

作者头像
鹏-程-万-里
发布2019-09-27 12:19:37
6760
发布2019-09-27 12:19:37
举报

这周的内容还是蛮有意思的!构建一个聊天室,如果我们20年前掌握了这篇文章的内容,那我们就离马化腾不远了!哈哈哈!

一、基本概念:

1、网络:将不同区域的计算机连接起来,比如:局域网、城域网、互联网。

2、地址:IP地址 确定网络上的一个绝对地址类似于:房子。

3、端口号:区分计算机不同软件,类似于房子的房门。端口号长度为2个字节,范围为:0---65535,共有65536个。

tips:在使用户端口号的时候,在同一个协议下,端口号不能重复,如果在不同协议下,则端口号可以重复。1024以下的端口号不要使用,主要是留给设备服务商使用的固定端口号

4、资源定位:

URL:统一资源定位符

URI:统一资源

5、数据的传输:

TCP协议:类似于打电话,有三次握手机制,面向连接,安全可靠,效率低下。

UDP协议:类似于发短信,非面向连接,效率高,但是不可靠,可能存在信息丢失的情况。

二、网络编程中的一些基本类

1、地址及端口:

(1)InetAddress:封装计算机的ip地址和DNS,没有端口

方法:

getLocalHost():获取本地地址

getHostName():返回域名

getHostAddress():返回IP地址

getByName():通过域名或者IP来获取地址

(2)InetSocketAddress:在InetAddress基础上+端口

创建对象:

InetSocketAddress(String hostname, int port)

InetSocketAddress(InetAddress addr, int port)

方法:

getHostName():获取域名

getPort():获取端口号

getAddress():获取InetAddress对象

2、URL:

四部分组成: 协议 存放资源的主机域名 端口 资源文件名(/)

(1)创建

URL(String spec):绝对路径构建

URL(URL context, String spec):相对路径构建

(2)方法

代码语言:javascript
复制
package com.peng.net.url;

import java.net.MalformedURLException;
import java.net.URL;

public class URLDemo01 {

  public static void main(String[] args) throws MalformedURLException {
    //绝对路径构建
    URL url = new URL("http://www.baidu.com:80/index.html#aa?uname=peng");
    System.out.println("协议:"+url.getProtocol());
    System.out.println("域名:"+url.getHost());
    System.out.println("端口:"+url.getPort());
    System.out.println("资源:"+url.getFile());
    System.out.println("相对路径:"+url.getPath());
    System.out.println("锚点:"+url.getRef());
    System.out.println("参数:"+url.getQuery());//如果存在锚点,则将参数视为锚点的一部分,返回null;如果不存在锚点,则返回参数  
    //相对路径
    url = new URL("http://www.baidu.com/a/");
    url = new URL(url,"b/c.txt");
    System.out.println(url.toString());   
  }
}

三、UDP编程,基本概念:

UDP:以数据为中心,非面向连接,不安全,数据可能丢失,效率高。

1、客户端

1)创建客户端 DatagramSocket 类 +指定发送端口

2)编辑数据 字节数组

3)打包 DatagramPacket + 服务器地址 + 指定的接收端口

4)发送数据

5)释放资源

2、服务器端

1)创建服务器端 DatagramSocket 类 + 指定接收端口

2)创建接收容器 字节数组

3)打包封装 DatagramPacket

4)包 接收数据

5)分析

6)释放资源

由于UDP协议编程是非面向连接的,TCP协议编程面向连接,相比之下TCP更加复杂,所以此处不放入UDP编程进行讲解,我们结合后面的TCP编程进行解析UDP编程细节。

四、基于TCP编程:

面向连接 安全可靠 效率低,类似于打电话

1、面向连接:请求-响应 Request--Response

2、Socket编程

1)、服务器:SeverSocket

2)、客户端:Socket

基本的TCP相关协议我们在下面一个实例中进行讲解——聊天室创建,其中包含有群聊和私聊功能。

基本的通讯思路如下图所示:

在客户端首先和服务器端建立连接通道,也就是socket,然后在传输通道中进行数据的传输,每一个通道内的蓝色箭头,代表着数据的输入和输出流。并且在数据的发送和接收过程中,可以同时进行,不会受到彼此的影响。在同一个聊天室中,具有多个客户端,他们需要同时连接在我们的服务器端上,因此我们在设计的过程中需要进行多线程的应用。

第一步:我们首先对客户端的接收数据进行封装,创建接收通道。

代码语言:javascript
复制
package com.peng.net.tcp.chat.demo04;
/**
 * 从服务器接收数据
 */

import java.io.DataInputStream;
import java.io.IOException;
import java.net.Socket;

public class Receive implements Runnable{
  //管道输入流
  private DataInputStream dis ;
  //线程标识符
  private boolean isRunning = true;
  
  //构造器
  public Receive() {
    
  }
  public Receive(Socket client) {
    try {
      //获取客户端与服务器之间的传输管道
      dis = new DataInputStream(client.getInputStream());
    } catch (IOException e) {
      isRunning = false;
      CloseUtil.closeAll(dis);
    }
  }
  
  /**
   * 获取从服务器发送到客户端的数据
   * @return
   */
  public String receive() {
    String msg = "";
    try {
      msg = dis.readUTF();//从管道输入流中读取发送过来的数据内容
    } catch (IOException e) {
      isRunning = false;
      CloseUtil.closeAll(dis);
    }
    return msg;
  }
   
  @Override
  public void run() {
    //线程体
    while(isRunning) {
      System.out.println(receive());//在客户端的控制台上打印接收到的数据内容
    }
    
  }
 
}

解析:在接收数据的过程中,我们主要思路是,在构造器中对输入流进行初始化操作,应用“DataInputStream”输入流,然后加入一个接收方法,将管道中服务器传回来的数据进行读取,最后在线程体中,将读取到的内容传输到客户端的界面上。

由于我们在多线程的使用中,频繁使用关闭输入输出流的关闭操作,所以我们将输入输出流的关闭操作封装成为一个单独的类,这样便于我们后期的调用和处理。

代码语言:javascript
复制
package com.peng.net.tcp.chat.demo04;

import java.io.Closeable;
import java.io.IOException;

/**
 * 关闭流
 */
public class CloseUtil {
  
  public static void closeAll(Closeable... io) {
    for (Closeable tem:io) {
      try {
        if(null !=tem ) {
          tem.close();
        }
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  }
}

tips:在对其进行封装的过程中,注意我们使用到了代码“Closeable...”,其中的运算符“...”相当于数组“[]”。

第二步:我们对客户端的发送操作进行一个封装操作。

代码语言:javascript
复制
package com.peng.net.tcp.chat.demo04;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;

/**
 * 发送数据线程
 */
public class Send implements Runnable{
  //控制台输入流
  private BufferedReader console ;
  //管道输出流
  private DataOutputStream dos ;
  //客户端的名称
  private String name;
  //线程标识符
  private boolean isRunning = true;

  //构造器:初始化输入输出流
  public Send() {
    console = new BufferedReader(new InputStreamReader(System.in));
  }
  public Send(Socket client,String name) {
    this();
    try {
      //初始化客户端向服务器端发送信息的管道
      dos = new DataOutputStream(client.getOutputStream());
      this.name = name;
      this.send(this.name);//在接收到名称时,就将客户端的名称发送出去
    } catch (IOException e) {
      isRunning = false;
      CloseUtil.closeAll(dos,console);
    }
  }
  
  /**
   * 从控制台获取数据
   * @return
   */
  public String getMsgFromConsole() {
    try {
      return console.readLine();//获取控制台输入的数据
    } catch (IOException e) {
      isRunning = false;
      CloseUtil.closeAll(dos,console);      
    }
    return "";
  }
  
  /**
   * 将客户端的数据发送给服务器
   * @param info
   */
  public void send(String info) {
    try {
      if(null != info && !info.equals("")) {
        dos.writeUTF(info);//写出数据信息
        dos.flush();//强制刷新
      }
    } catch (IOException e) {
      isRunning = false;
      CloseUtil.closeAll(dos,console);
    }
    
  }
  
  @Override
  public void run() {
    //线程体
    while(isRunning) {
      send(getMsgFromConsole());
    }
    
  }

}

解析:在进行发送操作的时候,我们需要有两个流操作,一个是输入流,主要负责从控制台上接收客户端输入的数据,另一个是输出流,主要负责将从客户端上获取到的信息发送到服务器进行操作。所以我们为了降低方法之间的耦合性,使用了两个方法,分别封装其功能。在最后的线程体中,我们将接收到的数据直接发送给客户端。注意,我们在构造器中发送了一个名称给客户端,这一点在我们创建客户端的代码中会进行解释。

第三步:创建客户端

代码语言:javascript
复制
package com.peng.net.tcp.chat.demo04;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;


/**
 * 创建客户端:发送数据+接收数据
 * 写出数据:输出流
 * 读取数据:输入流
 *  同时需要将输入流和输出流分别封装起来,彼此独立,相互独立处理
 *  加入客户端名称
 */
public class Client {

  public static void main(String[] args) throws IOException {
    System.out.println("请输入名称:");
    //从控制台输入客户端名称
    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));//新建输入流    
    String name = br.readLine();//获取客户端名称
    if("" == name) {//如果名字为空,则退出
      return;
    }
    
    //创建客户端,与服务器进行连接,并指定端口号
    Socket client = new Socket("localhost",9999);
    new Thread(new Send(client,name)).start();//发送路径
    new Thread(new Receive(client)).start();//接受路径
    
  }

}

解析:在创建客户端的时候,我们首先需要获取每一个客户端的名称,在获取到名称之后,我们立刻将客户端的名称发送给服务器后,服务器会进行一定的反馈,返回给客户端的消息为:“欢迎加入聊天室”,然后在其他客户端的界面上,输出“XXX加入了聊天室”。

tips:在UDP协议中,客户端发送数据的时候,需要指定客户端发送端口,以及服务器的接收端口,这一点与TCP协议编程中有所不同。在TCP编程中,客户端不需要指定对应的发送端口,系统会自动分配给客户端端口,但是并非TCP不要需要端口,只是开发者在编程的时候可以省略而已。

第四步:创建服务器

代码语言:javascript
复制
package com.peng.net.tcp.chat.demo04;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;

/**
 * 创建服务器 加入多线程 
 * 实现多个不同的客户端可以同时进行发送接收数据
 */
public class Server {
  //存储所有客户端与服务器建立的管道
  private List<MyChannel> all = new ArrayList<MyChannel>();

  public static void main(String[] args) throws IOException  {
    new Server().start();
  }
  
  public void start() throws IOException {
    //创建服务器,指定端口号
    ServerSocket server = new ServerSocket(9999);
    while(true) {
      Socket client = server.accept();//接收客户端请求,并与客户端建立连接
      MyChannel channel = new MyChannel(client);
      all.add(channel);//向容器中加入客户端通道,便于统一管理
      new Thread(channel).start();//一条道路
    }
  }

  /**
   * 定义匿名内部类,便于调用类的属性,此匿名类相当于客户端和服务器之间建立的道路
   * 一个客户,一条道路
   * 1、输入流:接收数据
   * 2、输出流:发送数据
   */
  private class MyChannel implements Runnable{
    //输入流:接收数据
    private DataInputStream dis;
    //输出流:发送数据
    private DataOutputStream dos;
    //客户端的名称
    private String name;
    //线程运行标识符
    private boolean isRunning = true;
    
    //构造器
    public MyChannel(Socket client) {
      try {
        dis = new DataInputStream(client.getInputStream());
        dos = new DataOutputStream(client.getOutputStream());
        
        this.name = dis.readUTF();//获取客户端名称
        this.send("欢迎加入聊天室");//向本客户端发送此信息
        this.sendAll(this.name+"加入了聊天室",true);//向其他的客户端发送该信息
        
      } catch (IOException e) {
        isRunning = false;
        CloseUtil.closeAll(dis,dos);
        all.remove(this);//移除通道自身
      }
    }

    /**
     * 接收从客户端发送过来的信息
     * @return
     */
    public String receive () {
      String msg = "";
      try {
        msg = dis.readUTF();//获取读入的信息
        send("you say :"+msg);
      } catch (IOException e) {
        isRunning = false;
        CloseUtil.closeAll(dis,dos);
        all.remove(this);//移除通道自身
      }
      return msg;
    }

    /**
     * 向本客户端发送相关信息
     * @param msg
     */     
    public void send(String msg) {      
      try {
        if(null != msg && !msg.equals("")) {
          dos.writeUTF(msg);
          dos.flush();
        }else {
          return;
        }
      } catch (IOException e) {
        isRunning = false;
        CloseUtil.closeAll(dis,dos);
        all.remove(this);//移除通道自身
      }
    }    
    
    /**
     * 向除本客户端以外的其他客户端发送信息
     * 根据发送的消息区分是私聊还是群聊
     * 在群发的消息中,使用flag区分该消息是服务器的系统消息,还是用户群聊的信息
     * @param msg
     * @param flag
     */  
    public void sendAll(String msg,boolean flag) {
      //约定,如果发送的信息中包含有“@name:.....”,则将name取出,然后与该用户进行私聊
      if(msg.startsWith("@") && msg.indexOf(":")>-1) {//私聊
        String name = msg.substring(1,msg.indexOf(":"));//获取私聊对象的名称
        String contents = msg.substring(msg.indexOf(":")+1);//获取私聊的内容
        for(MyChannel other:all) {//遍历所有客户端
          if(other.name.equals(name)) {//存在将要私聊的对象
            other.send(this.name+"对您悄悄说:"+contents);
            return;
          }
        }
        this.send("当前聊天室中不存在此用户");
      }else {//群聊
        if(flag) {//属于系统消息
          for(MyChannel other:all) {
            if(this == other) {//跳过本客户端自身
              continue;
            }
            //将本客户端发送的数据,发送给其他已经加入聊天的客户
            other.send("系统消息:"+msg);
          }
        }else {
          for(MyChannel other:all) {
            if(this == other) {//跳过本客户端自身
              continue;
            }
            //将本客户端发送的数据,发送给其他已经加入聊天的客户
            other.send(this.name+"对大家说:"+msg);
          }
        }
        
      }
    }

    @Override
    public void run() {
      //线程体
      while(isRunning) {        
        sendAll(receive(),false);
      }

    }

  }

}

解析:

1、正如我们对聊天室的功能分析上,聊天室应该具有群聊和私聊的基本功能。所以我们根据客户端发送的消息进行区分是私聊还是群聊,具体的规则为:服务器获取到客户端发送进来的数据,然后如果该消息以“@XXX:”开头,则获取“@”和“:”中间的名称"XXX",然后在所有客户端中进行搜索名称为“XXX”的客户端,服务器将该消息仅仅转发给客户”XXX“。

2、在我们管理聊天室中的所有客户的时候,我们使用了容器List进行统一管理。但是这里在导入包的时候,一定要注意,此处导入的是容器类包java.util.List。在我们使用自动导包过程时,eclipse给我们的提示中,还有一个是java.awt.List,这个包是java中GUI界面操作的工具类包,千万要注意此处的导包,一旦导错之后,很难检查出错误。

tips:查看源码,可以对比出两个包继承关系以及实现接口之间的差别,进入源码中查询可以看出:

java.util.List中继承关系为:interface List<E> extends Collection<E>,主要是实现相应的容器类;

而java.awt.List继承和实现关系为:List extends Component implements ItemSelectable, Accessible,主要是实现GUI图形界面的工具类

第五步:运行查看一下相关的结果

a客户端控制台信息:

b客户端控制台信息:

c客户端控制台信息:

解析:由于我们使用的是tcp协议,需要客户端先建立连接之后,才可以进行相互通讯传输数据。所以在测试的时候,需要我们首先需要运行服务器,使服务器处于就绪状态,随时接受来自客户端的请求,然后再创建客户端进行操作,否则会报错。我们在测试的时候,创建了3个客户端,分别是“a”、“b”、“c"。在测试的时候,我们使用a客户端给b发送了hello,然后可以在b客户端看到a发送过来的私聊信息,而c客户端界面上没有出现这条信息,所以完成了私发消息的功能。然后使用c客户端发送了信息a beautiful world,该信息属于群发信息,所以出现在了a和b客户端的窗口。然后在c窗口中,对一个不存在的对象d进行发送信息,可以看到服务器返回的信息:当前聊天室中不存在该用户。系统具有一定的容错率。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-03-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Java小白成长之路 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档