【Android】Socket实现简易聊天室功能

作者:邹峰立,微博:zrunker,邮箱:zrunker@yahoo.com,微信公众号:书客创作,个人平台:www.ibooker.cc

本文选自书客创作平台第31篇文章。阅读原文

书客创作

简易聊天室,什么是聊天室呢,简单一点说就是一些人可以共同聊天,别人能够看见你发布的消息,你也可以看到别人的消息,大家的消息是公开的。

功能分析:

1、聊天功能,聊天是一个长时间的相互交互的过程,要实现长时间连接Socket是一个比较不错的选择。

2、一些人相互聊天功能,要想实现相互聊天,就要将消息转发给所有建立连接的人,这里就要进行消息转发。

实现思路:

利用Socket实现客户端和服务端长连接,每次将连接进来的Socket保存到一个集合当中,当其中一个Socket有接收到数据的时候,将接收到的数据转发给其他的Socket,这样既能实现聊天功能又能实现转发。

对Socket的操作应放在子线程当中,每一次有新的Socket连接进来之后就新开一个子线程。

如果对Socket不是很了解,推荐【JavaEE】Socket简单体验, TCP和UDP这篇文章进行了解。

服务端搭建

首先要实现监听客户端连接,需要利用ServerSocket的accept方法进行监听,但是该方法会将程序阻塞,所以该监听过程要放在子线程中完成。这里定义的线程为ServerListener。

其次每监听到一个Socket,Socket无论是获取还是传输过程都是对字节流进行操作,这是一个比较耗时的操作,所以也要放在子线程中操作。这里定义的线程为ChatSocket。

最后还要定义一个Socket的子线程管理类,方便对所有连接进来的Socket的子线程进行管理,例如对数据的转发。这里定义的管理类为ChatManager。

在程序最开始运行的时候,要开启对Socket连接的监听,所以在main方法中开始监听线程。

public static void main(String[] args) {
   // 开启服务端监听
   new ServerListener().start();
}

开启监听线程之后,当监听到有Socket进行连接的时候,不仅要新建一个子线程实现该Socket的操作,而且要把该Socket添加到Socket线程管理类ChatManager当中,方便后期管理。

public class ServerListener extends Thread {
   @Override
   public void run() {
      try {
         // 1、创建ServerScoket,设置端口
         ServerSocket serverSocket = new ServerSocket(12345);
         while (true) {
            // 2、accept方法将导致程序阻塞
            Socket socket = serverSocket.accept();
            JOptionPane.showMessageDialog(null, "有客户端连接到本机的12345端口");
            // 3、将socket传递给新线程
            ChatSocket cs = new ChatSocket(socket);
            cs.start();
            // 4、使用Chatmanager进行管理
            ChatManager.getInstance().add(cs);
         }
      } catch (IOException e) {e.printStackTrace();}
   }
}

在ChatSocket新线程中,要实现对接收到的Socket进行处理,例如获取Socket客户端发送过来的数据进行转发等。而转发过程,是转发给所有连接进来的Socket,所以转发过程的实现将由ChatManager完成。

ChatSocket在一开启线程的时候,就要进行客户端数据获取,并转发。

ChatSocket要能够完成服务端想客户端发送数据的过程。

public class ChatSocket extends Thread {
   private Socket socket;
   public ChatSocket(Socket socket) {
      this.socket = socket;
   }
   // 服务端传值给客户端
   public void out(String out) {
      try {
         if (socket.isConnected() && !socket.isClosed()) {
            // 获取当前Socket输出流,输出数据
            socket.getOutputStream().write(out.getBytes("gbk"));
            System.out.println("转发数据**********" + out);
         } else {
            // 链接已关闭
            ChatManager.getInstance().remove(this);
         }
      } catch (IOException e) {
         e.printStackTrace();
      }
   }
   @Override
   public void run() {
      try {
         if (socket.isConnected() && !socket.isClosed()) {
            // 接受客户端数据
            BufferedReader br = new BufferedReader(new InputStreamReader(
socket.getInputStream(), "gbk"));
            // 读取数据
            String line = null;
            while ((line = br.readLine()) != null) {
               // 转发数据
               ChatManager.getInstance().publish(this, line);
               System.out.println("接受数据******" + line);
            }
            br.close();
         } else {
            // 链接已关闭
            ChatManager.getInstance().remove(this);
         }
      } catch (UnsupportedEncodingException e) {
         e.printStackTrace();
      } catch (IOException e) {
         e.printStackTrace();
      }
   }
}

最后来说说Socket线程管理类ChatManager,一个聊天室只能有一个Socket线程管理类,所以该类要使用单例模式。

private static ChatManager cm;
// 单例
public static ChatManager getInstance() {
   if (cm == null) {
      cm = new ChatManager();
   }
   return cm;
}

提供Socket子线程存放集合,能够实现Socket子线程添加和移除功能。

// ChatSocket集合
Vector vector = new Vector<>();
// 添加ChatSocket
public void add(ChatSocket chatSocket) {
   vector.add(chatSocket);
}
// 移除ChatSocket
public void remove(ChatSocket chatSocket) {
   vector.remove(chatSocket);
}

最后是借助Socket子线程存放集合,实现数据转发功能。

// 发送消息
public void publish(ChatSocket cs, String out) {
   for (int i = 0; i < vector.size(); i++) {
      ChatSocket chatSocket = vector.get(i);
      if (!cs.equals(chatSocket)) {
         chatSocket.out(out);
      }
   }
}

Android端搭建

对于Android客户端而言就显得稍微简单一些,只需要实现数据的接受和传递即可。

首先是界面的构建:

界面的构建

客户端界面
当点击连接的时候,客户端会向服务端发送连接请求,请求成功之后便可发送消息,聊天内容将会在界面中间部分显示。

1、初始化信息,在Activity中完成对控件的初始化和点击事件监听。

// 定义控件全局变量
private TextView ipTv, contentTv;
private Button linkBtn, sendBtn;
private EditText sendEd;
// 初始化控件
private void initView() {
   ipTv = (TextView) findViewById(R.id.tv_ip);
   contentTv = (TextView) findViewById(R.id.tv_content);
   linkBtn = (Button) findViewById(R.id.btn_link);
   linkBtn.setOnClickListener(this);
   sendBtn = (Button) findViewById(R.id.btn_send);
   sendBtn.setOnClickListener(this);
   sendEd = (EditText) findViewById(R.id.ed_send);
}
// 点击事件监听
@Override
public void onClick(View v) {
   switch (v.getId()) {
      case R.id.btn_link:// 链接
         connect();
         break;
      case R.id.btn_send:// 发送
         send();
         break;
      }
}

2、连接服务端,并获取服务端传递数据。

// 定义三个全局变量
private Socket socket;
private BufferedReader reader;
private BufferedWriter writer;
// 连接Socket服务端
private void connect() {
   final String ip = ipTv.getText().toString().trim();
   final int port = 12345;
   // 异步执行
   AsyncTask asyncTask = new AsyncTask() {
      @Override
      protected Void doInBackground(Void... params) {
         try {
            // 实例化Socket
            socket = new Socket(ip, port);
            reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            publishProgress("success");
            // 读取传递过来的数据
            String line;
            while ((line = reader.readLine()) != null) {
               publishProgress(line);
            }
         } catch (IOException e) {
            publishProgress("failed");
            e.printStackTrace();
         }
         return null;
      }
      @Override
      protected void onProgressUpdate(String... values) {
         if ("success".equals(values[0]))
            Toast.makeText(MainActivity.this, "连接成功", Toast.LENGTH_LONG).show();
         else if ("failed".equals(values[0]))
            Toast.makeText(MainActivity.this, "无法建立连接", Toast.LENGTH_LONG).show();
         else
            contentTv.append("他说:" + values[0] + "\n");
         super.onProgressUpdate(values);
      }
   };
   asyncTask.execute();
}

注意连接服务端和都去服务端数据是一个耗时的操作,所以应放在子线程中完成,这里是采用异步AsyncTask的方式来进行处理。

3、发送数据

// 向Socket服务端发送
private void send() {
   String out = sendEd.getText().toString().trim();
   // 发送数据
   try {
      writer.write(out + "\n");
      writer.flush();
      sendEd.setText("");
      contentTv.append("我说:" + out + "\n");
   } catch (IOException e) {
      e.printStackTrace();
   }
}

到这里就已经全部完成了,包括服务端和Android客户端的实现,在实际开发当中往往是通过构建一些比较成熟的框架来实现这一过程,实现原理大致相同,实现过程有所不同。

Github后台代码地址
Github安卓客户端代码地址
阅读原文


微信公众号:书客创作
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,723评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,003评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,512评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,825评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,874评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,841评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,812评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,582评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,033评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,309评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,450评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,158评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,789评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,409评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,609评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,440评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,357评论 2 352

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,043评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,649评论 18 139
  • 之前就觉得docker 是个很吊的东西,也在电脑上装了docker,但是一直都比较忙,所以也忘了,最近突然想起就重...
    samYau阅读 617评论 0 3
  • 今天第一次发现有简书这个软件,下载打开之后有个叫手账的词语引起了我的注意,通过网络我了解到什么是手帐,所以试着写属...
    baby琴阅读 216评论 0 1
  • to my regret , the years passed my english is still poor ...
    Singaforever阅读 153评论 0 0