青春互撩——详解基于Socket通信的聊天软件开发(附项目源码)

自定义View系列教程00–推翻自己和过往,重学自定义View
自定义View系列教程01–常用工具介绍
自定义View系列教程02–onMeasure源码详尽分析
自定义View系列教程03–onLayout源码详尽分析
自定义View系列教程04–Draw源码分析及其实践
自定义View系列教程05–示例分析
自定义View系列教程06–详解View的Touch事件处理
自定义View系列教程07–详解ViewGroup分发Touch事件
自定义View系列教程08–滑动冲突的产生及其处理


探索Android软键盘的疑难杂症
深入探讨Android异步精髓Handler
详解Android主流框架不可或缺的基石
站在源码的肩膀上全解Scroller工作机制


Android多分辨率适配框架(1)— 核心基础
Android多分辨率适配框架(2)— 原理剖析
Android多分辨率适配框架(3)— 使用指南


Android程序员C语言自学完备手册
讲给Android程序员看的前端系列教程(图文版)
讲给Android程序员看的前端系列教程(视频版)


版权声明


项目概述

青春互撩是基于Socket开发的局域网聊天软件。

具体功能如下:

  • 好友上线提醒
  • 聊天消息显示
  • 局域网内单聊
  • 局域网内群聊
  • 聊天内容清屏

服务端主要操作:

  • 接受客户端的连接请求
  • 接收来自客户端的消息
  • 转发消息至指定客户端
  • 实时更新在线用户

客户端主要操作:

  • 发送消息至指定用户
  • 接收服务端发送的消息
  • 清除当前聊天记录
  • 退出聊天并终止与服务端的连接

图示如下:

在这里插入图片描述

在这里插入图片描述
嗯哼,看完效果图后我们先来学习相关基础知识再进行项目开发。


socket

在这里插入图片描述

如上图,在七个层级关系中我们将socket归属于传输层。socket是基于应用服务与TCP/IP通信之间的一个抽象,它将TCP/IP协议里面复杂的通信逻辑进行了分装。socket是网络程序之间双向通信的最后终结点,它由地址(IP)和端口号(PORT)组成。

对于socket编程我们有两种通信协议可以选择:

  • UDP(User Data Protocol 用户数据报协议)
  • TCP(Transfer Control Protocol 传输控制协议)

UDP

概述

UDP协议是一种对等通信的实现,发送方只需要知道接收方的IP和Port,就可以直接向它发送数据,不需要事先连接。每个程序都可以作为服务器,也可作为客户端。UDP是一种无连接的传输协议,它以数据报(DatagramPacket)作为数据传输的载体。数据报的大小是受限制的,每个数据报的大小限定在64KB以内。

使用UDP协议进行数据传输时,需将要传输数据定义为数据报(DatagramPacket),在数据报中指明数据所要达到的Socket再将数据报发送出去。在接收到发送方的数据报(DatagramPacket)时,不仅可以获得数据还可获得发送方的IP和Port,这样就可以向发送方发送数据。因此,从本质上而言发送方与接收方两者是对等的。

UDP中每个数据报都是一个独立的信息,包括完整的源地址或目的地址,它在网络上以任何可能的路径传往目的地,因此能否到达目的地,到达目的地的时间以及内容的正确性都是不能被保证的。这种传输方式是无序的,也不能确保绝对的安全可靠,但它很简单也具有较高的效率,这与通过邮局发送邮件的情形非常相似。

但是由于UDP的特性:它不属于连接型协议,因而具有资源消耗小,处理速度快的优点,所以通常音频、视频和普通数据在传送时使用UDP较多,因为它们即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。

实现

UDP通信的Socket使用DatagramSocket类实现,数据报使用DatagramPacket类实现。

示例

发送方

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

/*
 * 利用UDP协议发送数据:
 * 第一步:创建发送端Socket对象
 * 第二步:创建数据,并把数据打包
 * 第三步:调用Socket对象的发送方法发送数据包
 * 第四步:释放资源
 * 
 * 本文作者:谷哥的小弟
 * 博客地址:https://blog.csdn.net/lfdfhl
 */
public class SendMessage {

	public static void main(String[] args) throws IOException {
		// 创建发送端Socket对象
		DatagramSocket datagramSocket = new DatagramSocket();
		// 准备数据
		String message = new String("大家好");
		byte[] messageByte = message.getBytes();
		int len = messageByte.length;
		// 通信地址
		InetAddress address = InetAddress.getLocalHost();
		int port=10088;
		// 打包数据
		DatagramPacket datagramPacket = new DatagramPacket(messageByte, len, address, port);
		// 发送数据
		datagramSocket.send(datagramPacket);
		// 关闭socket
		datagramSocket.close();
	}

}

接收方

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

/*
 * 利用UDP协议接收数据:
 * 第一步:创建接收端Socket对象
 * 第二步:创建一个数据包用于接收数据
 * 第三步:调用Socket对象的接收方法接收数据
 * 第四步:解析数据包,并显示在控制台
 * 第五步:释放资源
 * 
 * 本文作者:谷哥的小弟
 * 博客地址:https://blog.csdn.net/lfdfhl
 */
public class ReceiveMessage {

	public static void main(String[] args) throws IOException {
		int port=10088;
		DatagramSocket datagramSocket=new DatagramSocket(port);
		byte [] receiveByte=new byte[1024*10];
		DatagramPacket datagramPacket=new DatagramPacket(receiveByte, receiveByte.length);
		datagramSocket.receive(datagramPacket);
		byte[] data = datagramPacket.getData();
		int length = datagramPacket.getLength();
		String receivedData=new String(data,0,length);
		InetAddress address = datagramPacket.getAddress();
		String ip = address.getHostAddress();
		System.out.println("接收到来自:"+ip+"的数据,内容为:"+receivedData);
		datagramSocket.close();
	}

}


TCP

概述

TCP是一种面向连接的保证可靠传输的协议。通过TCP协议传输,得到的是一个顺序的无差错的数据流。发送方和接收方的成对的两个socket之间必须建立连接,以便在TCP协议的基础上进行通信,当一个socket(通常都是server socket)等待建立连接时,另一个socket可以要求进行连接,一旦这两个socket连接起来,它们就可以进行双向数据传输并且双方都可以进行发送或接收操作。TCP通讯类似于打电话,必须双方把电话接通后才能进行通话,任何一方断线都会造成无法进行通话,须再次连接。

TCP协议用来控制两个网络设备之间的点对点通信,两端设备按作用分为客户端和服务端。服务端为客户端提供服务,通常等待客户端的请求消息,有客户端请求到达后,及时提供服务和返回响应消息;客户端向服务端主动发出请求,并接收服务的响应消息。

实现

TCP通信过程中采用Socket和ServerSocket实现。无论一个TCP通信程序的功能多么齐全、程序多么复杂,其基本原来和结构都是类似的,都包括以下四个基本步骤:

第一步:
在服务端指定一个端口号来创建ServerSocket,并使用accept方法进行侦听,这将阻塞服务器线程,等待用户请求。

第二步:
在客户端指定服务器的主机IP和端口号来创建Socket,并连接到服务端ServerSocket,现在服务端的accept方法将被唤醒,同时返回一个和客户端通信的Socket。

第三步:
在客户端和服务端分别使用Socket来获得网络通信的输入/输出流,并按照一定的通信协议对Socket进行读/写操作。

第四步:
在通信完成后,在客户端和服务端中分别关闭Socket

示例

客户端

import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;

/*
 * 利用TCP协议发送数据:
 * 第一步:创建发送端的Socket对象
 * 第二步:获取输出流,写数据
 * 第三步:释放资源
 * 
 * 本文作者:谷哥的小弟
 * 博客地址:https://blog.csdn.net/lfdfhl
 */
public class Client {
	public static void main(String[] args) throws Exception {
		InetAddress inetAddress=InetAddress.getLocalHost();
		String ip = inetAddress.getHostAddress();
		int port=10088;
		Socket socket=new Socket(ip,port);
		OutputStream outputStream =socket.getOutputStream();
		outputStream.write("您好".getBytes());
		socket.close();
	}
}

服务端

package com.tcp1;

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

/*
 * 利用TCP协议接收数据:
 * 第一步:创建接收端的Socket对象
 * 第二步:监听客户端连接。返回一个对应的Socket对象
 * 第三步:获取输入流,读取数据显示在控制台
 * 第四步:释放资源
 * 
 * 本文作者:谷哥的小弟
 * 博客地址:https://blog.csdn.net/lfdfhl
 */
public class Server {

	public static void main(String[] args) throws IOException {
		int port=10088;
		ServerSocket serverSocket=new ServerSocket(port);
		Socket socket = serverSocket.accept();
		InputStream inputStream = socket.getInputStream();
		byte [] b=new byte[1024];
		int len = inputStream.read(b);
		String message=new String(b,0,len);
		String ip = socket.getInetAddress().getHostAddress();
		System.out.println("收到来自"+ip+"的消息,内容为:"+message);
		socket.close();
		serverSocket.close();
	}

}


青春互撩项目实战

主要技术

本项目中涉及到以下主要技术如下:

GUI编程
利用GUI编程实现项目的UI部分。
主要知识点:JFrame、JButton、JLabel、JTextArea、JScrollPane、事件模型、事件响应。

网络编程
利用TCP通信实现消息的发送和接收。
主要知识点:网络基础知识、socket、ServerSocket

IO流
利用IO实现消息的读写和存储。
主要知识点:
InputStream、OutputStream

多线程
服务端采用多线程处理客户端发送的消息
线程的创建、多线程通信

通信协议
客户端与服务端之间通信时协议的设计与制定

项目代码结构

在这里插入图片描述

客户端实现

客户端界面编程

import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.io.OutputStream;
import java.net.Socket;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextArea;
import javax.swing.table.DefaultTableModel;
import cn.com.utils.Configuration;
import cn.com.utils.Util;
/**
 * 本文作者:谷哥的小弟
 * 博客地址:http://blog.csdn.net/lfdfhl
 */
class ClientFrame extends JFrame {
	private static final long serialVersionUID = -5451432026970178417L;
	private StringBuilder receiverStringBuilder;
	private Socket socket;
	private JButton sendButton;
	private JButton cleanButton;
	private JButton exitButton;
	private JLabel tipLabel;
	// 聊天消息框
	public JTextArea messageJTextArea;
	// 聊天消息框的滚动窗
	private JScrollPane messageJScrollPane;
	// 聊天文本输入框
	private JTextArea sendJTextArea;
	// 在线列表
	public JTable onlineJTable;
	// 在线列表的滚动窗
	private JScrollPane onlineJTableJScrollPane;
	
	public ClientFrame() {
		initFrame();
		initComponents();
	}

	private void initFrame() {
		// 标题
		setTitle("Young Chat");
		// 大小
		setSize(Configuration.CLIENT_FRAME_WIDTH, Configuration.CLIENT_FRAME_HEIGHT);
		// 不可缩放
		setResizable(false);
		// 设置布局:不使用默认布局,完全自定义
		setLayout(null);
	}

	private void initComponents() {
		initSendButton();
		initCleanButton();
		initExitButton();
		initTipLabel();
		initSendJTextArea();
		initMessageJTextArea();
		initOnlineJTable();
	}

	//发送消息
	private void initSendButton() {
		sendButton = new JButton("发送");
		sendButton.setBounds(20, 600, 100, 60);
		sendButton.setFont(new Font("宋体", Font.BOLD, 18));
		// 添加发送按钮的响应事件
		sendButton.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent event) {
				// 将JTextArea的滚动条拉至其底部
				messageJTextArea.setCaretPosition(messageJTextArea.getDocument().getLength());
				try {
					String receivers = receiverStringBuilder.toString();
					String message = sendJTextArea.getText();
					if (receivers != null && receivers.length() > 0) {
						// 在聊天窗显示与发送相关的信息
						messageJTextArea.append(Util.getCurrentTime() + Configuration.NEWLINE + "发往 " + receivers+ Configuration.NEWLINE);
						// 在聊天窗显示发送消息
						messageJTextArea.append(message + Configuration.NEWLINE);
						// 向服务器发送聊天信息
						OutputStream out = socket.getOutputStream();
						out.write((Configuration.TYPE_CHAT + Configuration.SEPARATOR + receivers + Configuration.SEPARATOR+ message).getBytes());
					}
				} catch (Exception e) {

				} finally {
					// 清空文本输入框
					sendJTextArea.setText("");
				}
			}
		});
		this.add(sendButton);
	}

	
	private void initCleanButton() {
		cleanButton = new JButton("清屏");
		cleanButton.setBounds(140, 600, 100, 60);
		cleanButton.setFont(new Font("宋体", Font.BOLD, 18));
		// 添加清屏按钮的响应事件
		cleanButton.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent event) {
				messageJTextArea.setText("");
			}
		});
		this.add(cleanButton);
	}

	private void initExitButton() {
		exitButton = new JButton("退出");
		exitButton.setBounds(260, 600, 100, 60);
		exitButton.setFont(new Font("宋体", Font.BOLD, 18));
		// 添加退出按钮的响应事件
		exitButton.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent event) {
				try {
					// 向服务器发送退出信息
					OutputStream out = socket.getOutputStream();
					out.write((Configuration.TYPE_EXIT + Configuration.SEPARATOR).getBytes());
					System.exit(0);
				} catch (Exception e) {
				}
			}
		});
		this.add(exitButton);
	}

	private void initTipLabel() {
		tipLabel = new JLabel("亲,您想和谁聊天呢?");
		tipLabel.setBounds(20, 420, 300, 30);
		this.add(tipLabel);
	}

	private void initSendJTextArea() {
		sendJTextArea = new JTextArea();
		sendJTextArea.setBounds(20, 460, 360, 120);
		sendJTextArea.setFont(new Font("楷体", Font.BOLD, 16));
		this.add(sendJTextArea);
	}

	private void initMessageJTextArea() {
		messageJTextArea = new JTextArea();
		// 聊天消息框自动换行
		messageJTextArea.setLineWrap(true);
		// 聊天框不可编辑,只用来显示
		messageJTextArea.setEditable(false);
		// 设置聊天框字体
		messageJTextArea.setFont(new Font("楷体", Font.BOLD, 16));
		messageJScrollPane = new JScrollPane(messageJTextArea);
		// 设置滚动窗的水平滚动条属性:不出现
		messageJScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
		// 设置滚动窗的垂直滚动条属性:需要时自动出现
		messageJScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
		// 设置滚动窗大小和位置
		messageJScrollPane.setBounds(20, 20, 360, 400);
		// 添加聊天窗口的滚动窗
		this.add(messageJScrollPane);
	}

	private void initOnlineJTable() {
		// 当前在线用户列表的列标题
		String[] colTitles = { "IP", "端口" };
		onlineJTable = new JTable(new DefaultTableModel(null, colTitles) {
			private static final long serialVersionUID = -4350422327693462629L;
			// 设置表格不可编辑
			@Override
			public boolean isCellEditable(int row, int column) {
				return false;
			}
		});
		// 添加在线列表项被鼠标选中的相应事件
		onlineJTable.addMouseListener(new MouseListener() {
			@Override
			public void mouseClicked(MouseEvent event) {
				// 取得在线列表的数据模型
				DefaultTableModel tableModel = (DefaultTableModel) onlineJTable.getModel();
				// 提取鼠标选中的行作为消息目标(最少一个人,最多为全体在线者)
				int[] selectedIndex = onlineJTable.getSelectedRows();
				// 将所有消息目标的uid拼接成一个字符串,以逗号分隔
				receiverStringBuilder = new StringBuilder("");
				for (int i = 0; i < selectedIndex.length; i++) {
					String ip = (String) tableModel.getValueAt(selectedIndex[i], 0);
					String port = (String) tableModel.getValueAt(selectedIndex[i], 1);
					receiverStringBuilder.append(ip);
					receiverStringBuilder.append(":");
					receiverStringBuilder.append(port);
					if (i != selectedIndex.length - 1) {
						receiverStringBuilder.append(",");
					}
				}
				tipLabel.setText("消息发送至:" + receiverStringBuilder.toString());
			}

			@Override
			public void mousePressed(MouseEvent event) {
			};

			@Override
			public void mouseReleased(MouseEvent event) {
			};

			@Override
			public void mouseEntered(MouseEvent event) {
			};

			@Override
			public void mouseExited(MouseEvent event) {
			};
		});
		onlineJTableJScrollPane = new JScrollPane(onlineJTable);
		// 设置滚动窗的水平滚动条属性:不出现
		onlineJTableJScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
		// 设置滚动窗的垂直滚动条属性:需要时自动出现
		onlineJTableJScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
		// 设置当前在线列表滚动窗大小和位置
		onlineJTableJScrollPane.setBounds(420, 20, 250, 400);
		this.add(onlineJTableJScrollPane);
	}

	public Socket getSocket() {
		return socket;
	}

	public void setSocket(Socket socket) {
		this.socket = socket;
	}
}

客户端功能实现

import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.DefaultTableModel;
import cn.com.utils.Configuration;
import cn.com.utils.Util;
/**
 * 本文作者:谷哥的小弟
 * 博客地址:http://blog.csdn.net/lfdfhl
 */
public class Client {
	private Socket socket = null;
	private ClientFrame clientFrame;
	private InputStream socketInputStream;
	private OutputStream socketOutputStream;
	
	public static void main(String[] args) {
		Client client=new Client();
		client.initClientFrame();
		client.connectServer();
		client.handleReceivedMessage();
	}
	
	private void initClientFrame() {
		// 创建客户端窗口对象
		clientFrame = new ClientFrame();
		// 窗口关闭键无效,必须通过退出键退出客户端
		clientFrame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
		int screenWidth = Util.getScreenWidth();
		int screenHeight = Util.getScreenHeight();
		int x = (screenWidth - Configuration.CLIENT_FRAME_WIDTH) / 2;
		int y = (screenHeight - Configuration.CLIENT_FRAME_HEIGHT) / 2;
		// 设置位置
		clientFrame.setLocation(x, y);
		// 设置窗口可见
		clientFrame.setVisible(true);
	}
	
	private void connectServer() {
		try {
			// 连接服务器
			socket = new Socket(InetAddress.getLocalHost(), Configuration.PORT);
			clientFrame.setSocket(socket);
			// 获取输入流
			socketInputStream = socket.getInputStream();
			// 获取输出流
			socketOutputStream = socket.getOutputStream();
			// 获取服务端发送的欢迎信息
			byte[] buf = new byte[1024 * 1];
			int len = socketInputStream.read(buf);
			// 将欢迎信息显示在聊天消息框
			String welcomeMessage=new String(buf, 0, len);
			clientFrame.messageJTextArea.append(welcomeMessage);
			clientFrame.messageJTextArea.append(Configuration.NEWLINE);
		} catch (Exception e) {
			// TODO: handle exception
		}
	}
	
	private void handleReceivedMessage() {
		try {
			while (true) {
				byte[] buf = new byte[1024 * 1];
				socketInputStream = socket.getInputStream();
				int len = socketInputStream.read(buf);
				// 处理服务器传来的消息
				String message = new String(buf, 0, len);
				System.out.println("客户端收到消息 ---> "+message);
				int separatorIndex=message.indexOf(Configuration.SEPARATOR);
				// 消息类型:更新在线名单或者聊天
				String messageType = message.substring(0, separatorIndex);
				// 消息本体:最新的在线名单或者聊天内容
				String messageContent = message.substring(separatorIndex + 1);
				// 更新在线名单
				if (messageType.equals(Configuration.TYPE_UPDATEONLINELIST)) {
					// 得到在线列表的数据模型
					DefaultTableModel tableModel = (DefaultTableModel) clientFrame.onlineJTable.getModel();
					// 清空在线名单列表
					tableModel.setRowCount(0);
					// 更新在线名单
					String[] onlineArray = messageContent.split(",");
					// 添加当前在线者
					for (String online : onlineArray) {
						String[] stringArray = new String[2];
						if (online.equals(Util.getLocalHostAddress() + ":" + socket.getLocalPort())) {
							continue;
						}
						int colonIndex=online.indexOf(":");
						stringArray[0] = online.substring(0, colonIndex);
						stringArray[1] = online.substring(colonIndex + 1);
						tableModel.addRow(stringArray);
					}
					// 提取在线列表的渲染模型
					DefaultTableCellRenderer tableCellRenderer = new DefaultTableCellRenderer();
					// 表格数据居中显示
					tableCellRenderer.setHorizontalAlignment(JLabel.CENTER);
					clientFrame.onlineJTable.setDefaultRenderer(Object.class, tableCellRenderer);
				}
				// 聊天
				if (messageType.equals(Configuration.TYPE_CHAT)) {
					int messageContentSeparatorIndex=messageContent.indexOf(Configuration.SEPARATOR);
					String from = messageContent.substring(0, messageContentSeparatorIndex);
					String word = messageContent.substring(messageContentSeparatorIndex + 1);
					clientFrame.messageJTextArea.append(Util.getCurrentTime() + Configuration.NEWLINE + "来自 " + from+ Configuration.NEWLINE + word + Configuration.NEWLINE);
					clientFrame.messageJTextArea.setCaretPosition(clientFrame.messageJTextArea.getDocument().getLength());
				}
			}
		} catch (Exception e) {
			clientFrame.messageJTextArea.append("服务器异常");
			e.printStackTrace();
		}
	}


}

服务端实现

Server如下:

import java.net.ServerSocket;
import java.net.Socket;
import cn.com.utils.Configuration;
import cn.com.utils.Util;
/**
 * 本文作者:谷哥的小弟
 * 博客地址:http://blog.csdn.net/lfdfhl
 */
public class Server {
	public static void main(String[] args) throws Exception {
		// 建立服务器ServerSocket
		@SuppressWarnings("resource")
		ServerSocket serverSocket = new ServerSocket(Configuration.PORT);
		// 提示Server建立成功
		System.out.println("Server is running " + Util.getLocalHostAddress() + ":" + Configuration.PORT);
		// 监听端口,建立连接并开启新的HandleClientRunnable线程来服务此连接
		while (true) {
			// 接收客户端Socket
			Socket socket = serverSocket.accept();
			// 客户端IP
			String ip = socket.getInetAddress().getHostAddress();
			// 客户端端口
			int port = socket.getPort();
			// 建立新线程
			Runnable runnable=new HandleMessageRunnable(socket, ip, port);
			Thread handleClientThread=new Thread(runnable);
			handleClientThread.start();
		}
	}
}

HandleMessageRunnable如下:

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import cn.com.utils.Configuration;
import cn.com.utils.Util;
/**
 * 本文作者:谷哥的小弟
 * 博客地址:http://blog.csdn.net/lfdfhl
 */
public class HandleMessageRunnable implements Runnable {
	private Socket socket;
	private String ip;
	private int port;
	private InputStream socketInputStream;
	private OutputStream socketOutputStream;
	// 客户端ip和端口拼接得到其对应的uid
	private String clientUid = null;
	// 存储所有uid
	static ArrayList<String> clientUidArrayList = new ArrayList<String>();
	// HashMap存储所有uid, HandleMessageRunnable组成的键值对
	static HashMap<String, HandleMessageRunnable> hashMap = new HashMap<String, HandleMessageRunnable>();

	public HandleMessageRunnable(Socket socket, String ip, int port) {
		this.socket = socket;
		this.ip = ip;
		this.port = port;
		this.clientUid = ip + ":" + port;
	}

	@Override
	public void run() {
		try {
			addClient();
			sendConnectedMessage();
			updateOnlineList();
			handleMessage();
		} catch (Exception e) {
		}
	}
	
	public void addClient() {
		clientUidArrayList.add(clientUid);
		hashMap.put(clientUid, this);
	}
	
	public void removeClient() {
		int index = clientUidArrayList.indexOf(clientUid);
		clientUidArrayList.remove(index);
		hashMap.remove(clientUid);
	}
	
	public void sendConnectedMessage() throws IOException {
		// 获取输入流
		socketInputStream = socket.getInputStream();
		// 获取输出流
		socketOutputStream = socket.getOutputStream();
		// 向当前客户端传输连接成功信息
		String successMessage ="服务端提示信息:"+Configuration.NEWLINE+ Util.getCurrentTime() + Configuration.NEWLINE + "成功连接服务器" + Configuration.NEWLINE
				+ "服务器IP: " + Util.getLocalHostAddress() + ", 端口: " + Configuration.PORT + Configuration.NEWLINE
				+ "客户端IP: " + ip + ", 端口: " + port + Configuration.NEWLINE;
		socketOutputStream.write(successMessage.getBytes());
	}
	
	public void handleMessage() throws IOException {
		byte[] buf = new byte[1024];
		int len = 0;
		// 持续监听并转发客户端消息
		while (true) {
			len = socketInputStream.read(buf);
			String message = new String(buf, 0, len);
			System.out.println("服务端收到消息 ---> "+message);
			int separatorIndex=message.indexOf(Configuration.SEPARATOR);
			// 消息类型:退出或者聊天
			String messageType = message.substring(0, separatorIndex);
			// 消息本体:空或者聊天内容
			String messageContent = message.substring(separatorIndex + 1);
			// 根据消息类型分别处理
			if (messageType.equals(Configuration.TYPE_EXIT)) {
				// 更新ArrayList和HashMap, 删除退出的uid和线程
				removeClient();
				// 更新在线名单
				updateOnlineList();
				// 结束循环,结束该服务线程
				break;
			}
			// 聊天
			if (messageType.equals(Configuration.TYPE_CHAT)) {
				int messageContentSeparatorIndex=messageContent.indexOf(Configuration.SEPARATOR);
				// 提取收信者地址
				String[] receiveClientUidArray = messageContent.substring(0, messageContentSeparatorIndex).split(",");
				// 提取聊天内容
				String word = messageContent.substring(messageContentSeparatorIndex + 1);
				// 向收信者广播发出聊天信息
				sendChatMessage(clientUid, receiveClientUidArray, word);
			}
		}
	}

	
	// 向所有已连接的客户端更新在线名单
	public void updateOnlineList() throws IOException {
		// 将当前在线名单以逗号为分割组合成字符串
		StringBuilder stringBuilder = new StringBuilder(Configuration.TYPE_UPDATEONLINELIST + Configuration.SEPARATOR);
		for (String clientUid : clientUidArrayList) {
			stringBuilder.append(clientUid);
			int index = clientUidArrayList.indexOf(clientUid);
			int size =  clientUidArrayList.size();
			if (index != size - 1) {
				stringBuilder.append(",");
			}
		}
		String  onlineClients=stringBuilder.toString();
		for (String clientUid : clientUidArrayList) {
			OutputStream out = hashMap.get(clientUid).socket.getOutputStream();
			out.write(onlineClients.getBytes());
		}
	}

	// 向指定的客户端发送聊天消息
	public void sendChatMessage(String sendClientUid, String[] receiveClientUidArray, String word) throws IOException {
		for (String clientUid : receiveClientUidArray) {
			OutputStream out = hashMap.get(clientUid).socket.getOutputStream();
			String message=Configuration.TYPE_CHAT+Configuration.SEPARATOR + sendClientUid + Configuration.SEPARATOR + word;
			out.write(message.getBytes());
		}
	}
}

配置及工具

Configuration如下:

/**
 * 本文作者:谷哥的小弟
 * 博客地址:http://blog.csdn.net/lfdfhl
 */
public class Configuration {
	// 通信端口
	public final static int PORT = 10088;
	// 客户端窗口宽度
	public final static int CLIENT_FRAME_WIDTH = 700;
	// 客户端窗口高度
	public final static int CLIENT_FRAME_HEIGHT = 700;
	// 分割符
	public final static String SEPARATOR = "/";
	// 消息类型之更新在线列表
	public final static String TYPE_UPDATEONLINELIST = "updateOnlineList";
	// 消息类型之聊天
	public final static String TYPE_CHAT = "chat";
	// 消息类型之下线
	public final static String TYPE_EXIT="exit";
	// 回车换行
	public final static String NEWLINE="\r\n";
}

Util如下:

import java.awt.Toolkit;
import java.net.InetAddress;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
 * 本文作者:谷哥的小弟
 * 博客地址:http://blog.csdn.net/lfdfhl
 */
public class Util {
	//获取当前时间
	public static String getCurrentTime() {
		String time=null;
		SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		time=sdf.format(new Date());
		return time;
	}
	
	// 获取本机屏幕横向分辨率
	public static int getScreenWidth() {
		int screenWidth = Toolkit.getDefaultToolkit().getScreenSize().width;
		return screenWidth;
	}
	
	// 获取本机屏幕纵向分辨率
	public static int getScreenHeight() {
		int screenHeight = Toolkit.getDefaultToolkit().getScreenSize().height;
		return screenHeight;
	}
	
	// 获取本机IP地址
	public static String getLocalHostAddress() {
		String localHostAddress=null;
		try {
			localHostAddress=InetAddress.getLocalHost().getHostAddress();
		} catch (Exception e) {
			// TODO: handle exception
		}
		return localHostAddress;
	}
	
}


总结

本文先介绍了Socket通信技术以及TCP和UDP再通过一个完整示例实现了局域网内的聊天软件开发。总体来讲,该示例不算复杂;但是,要注意通信协议的设计和制定。另外,在本示例中我们使用到了IO流读取数据,建议有兴趣的童鞋深入研究IO并且关注NIO相关技术。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

谷哥的小弟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值