自定义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程序员看的前端系列教程(视频版)
版权声明
- 本文原创作者:谷哥的小弟
- 作者博客地址:http://blog.csdn.net/lfdfhl
项目概述
青春互撩是基于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相关技术。