通过这个简单的网络聊天室,来展示和说明Socket通信基本原理和执行方式。有什么不正不足之处,或者更好的见解,请各位牛人明示。文章的思路并非本人原创,仅仅为了自身对Socket通信编程的巩固,也借此分享给需要的人。
TCP/IP协议是一种可靠的网络协议,是在议通信的两端各建立一个Socket,在其之间形成一个网路虚拟链路,建立好之后
两端的程序就可以通过虚拟链路进行通信。
Java中使用Socket对象来代码两端的通信端口,且通过Socket产生的IO流进行网络通信。
TCP通信是没有服务端、客户端区分的。在没有正式建立起虚拟链路前,必须有一方主动接收其他通信实体的链接请求。在Java中,ServerSocket负责承担此类任务。它负责监听并接受来自客户端Socket的通信请求,如果没有通信请求,则一直处于等待状态。ServerSocket包含一个方法 accept() ,当接收到一个通信请求之后,此方法则会返回一个与客户端对应Socket对象,如果没有,则处于等待状态,线程也会处于阻塞状态直到有来自客户端的通信请求
public class Server
{
private static final int SERVER_PORT = 3003;
//使用MyMap对象来保存每个客户名字和对应输出流之间的对应关系。
public static YeekuMap<String , PrintStream> clients =
new YeekuMap<String , PrintStream>();
public void init()
{
ServerSocket ss = null;
try
{
//建立监听的ServerSocket
ss = new ServerSocket(SERVER_PORT);
//采用死循环来不断接受来自客户端的请求
int i = 0;
while(true)
{
Socket socket = ss.accept();
new ServerThread_1(socket).start();
System.out.println("---》接受一个客户端请求,创建一个线程:\t" + i);
i++;
}
}
//如果抛出异常
catch (IOException ex)
{
System.out.println("服务器启动失败,是否端口"
+ SERVER_PORT + "已被占用?");
}
//使用finally块来关闭资源
finally
{
try
{
if (ss != null)
{
ss.close();
}
}
catch (IOException ex)
{
ex.printStackTrace();
}
System.exit(1);
}
}
public static void main(String[] args)
{
Server server = new Server();
server.init();
}
}
用一个指定端口号来创建一个ServerSocket,端口号的有效数值应该是在0~65535
ServerSocket ss = new ServerSocket(3000);
建立一个类似与服务端的Sever类,在Server类的初始化模块中创建通信监听器,并在循环体中不断调用accept()方法接收来自其他客户端的通信请求,当接收到来自客户端的通信请求时accept()方法会返回一个与客户端对应的Socket通信实体,将其传入到一个通信线程中处理来自客户端的通信信息。如果没有接受到来自客户端的通信请求,其所在的线程将一直处于阻塞状态。
下面是服务器通信线程的管理类,主要负责处理来自客户端通信信息的处理:
package com.base.Net.Senior;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;
/**
* 服务器线程管理类
* @author Petter
*
*/
public class ServerThread_1 extends Thread{
private Socket socket;
BufferedReader buffReader;
PrintStream printStream;
/**
* 定义构造函数,用于接受一个Socket来创建ServerThread线程
* @param socket
*/
public ServerThread_1(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try{
//获取该Socket对应的输入流
buffReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
//获取该Socket对应的输出流
printStream = new PrintStream(socket.getOutputStream());
String line = null;
while( (line = buffReader.readLine()) != null ){
//如果读到的行以MyProtocol.USER_ROUND开始,并以其结束
//可以确定读到的是用户登录的用户名
if(line.startsWith(YeekuProtocol.USER_ROUND) && line.endsWith(YeekuProtocol.USER_ROUND)){
//获取真实的数据
String userName = getRealMsg(line);
System.out.println("---》用户: " + userName);
//如果用户名重复
if(Server.clients.containsKey(userName)){
System.out.println("登录的用户名重复!");
printStream.println(YeekuProtocol.NAME_REP);
}else{
System.out.println("登录到服务器成功!");
printStream.println(YeekuProtocol.LOGIN_SUCCESS);
Server.clients.put(userName, printStream);
}
}
//如果得到的行以YeeKuProtocol.PRIVATE_ROUND开始,并以起结束
//可以确定是私聊信息,私聊信息只向特定的输出流发送
else if(line.startsWith(YeekuProtocol.PRIVATE_ROUND) && line.endsWith(YeekuProtocol.PRIVATE_ROUND)){
//得到真实消息
String userAndMsg = getRealMsg(line);
//以SPLIT_SIGN来分割字符串,前面部分是私聊用户,后面部分是聊天信息
String user = userAndMsg.split(YeekuProtocol.SPLIT_SIGN)[0];
String msg = userAndMsg.split(YeekuProtocol.SPLIT_SIGN)[1];
//获取私聊用户对于的输出流
Server.clients.get(user).println(
Server.clients.getKeyByValue(printStream) + "悄悄的对你说:" + msg);
}
//公聊信息,向每个Socket发送
else{
//得到真实消息
String msg = getRealMsg(line);
//遍历Clients中的每个输出流
for(PrintStream clientsPs : Server.clients.valueSet()){
clientsPs.println(Server.clients.getKeyByValue(printStream) + "说:" + msg);
}
}
}
//捕获异常后, 表明该Socket对应的客户端已经出现了问题
//所以程序将对应的输出流从Map中删除
}catch(IOException e){
Server.clients.removeByValue(printStream);
System.out.println(Server.clients.size());
}finally{
if(buffReader != null){
try {
buffReader.close();
if(printStream != null){
printStream.close();
}
if(socket != null){
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 将读取到的内容去掉前后协议字符串,恢复真实的数据
* @param line
* @return
*/
public String getRealMsg(String line){
return line.substring(YeekuProtocol.protocol_len, line.length() - YeekuProtocol.protocol_len);
}
}
在通信线程管理类中,会用到 YeekuProtocol这样的通信协议类,主要是为了在接受到通信信息时区分信息种类,是公聊信息还是私聊
信息还是用户登录。
同时,为了将某个用户的公共聊天数据或者私密聊天数据发送给其他用户,在客户端做出请求时,会再通信线程管理类中将其对应的输出流PrintStream类实体,userName保存到一个键值对的Map中,这样就可以在服务端区分是哪个用户发送的通信信息,是以公共聊天的形式发送给其他所有在线用户还是 以私密形式发送给指定的在线用户
package com.base.Net.Senior;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
//扩展HashMap类,MyMap类要求value也不可重复
public class YeekuMap<K,V> extends HashMap<K,V>
{
/**
*
*/
private static final long serialVersionUID = -1731258447378994845L;
//根据value来删除指定项
public void removeByValue(Object value)
{
for (Object key : keySet())
{
if (get(key) == value)
{
remove(key);
break;
}
}
}
//获取所有value组成的Set集合
public Set<V> valueSet()
{
Set<V> result = new HashSet<V>();
//遍历所有key组成的集合
for (K key : keySet())
{
//将每个key对应的value添加到result集合中
result.add(get(key));
}
return result;
}
//根据value查找key。
public K getKeyByValue(V val)
{
//遍历所有key组成的集合
for (K key : keySet())
{
//如果指定key对应的value与被搜索的value相同
//则返回对应的key
if (get(key).equals(val)
&& get(key) == val)
{
return key;
}
}
return null;
}
//重写HashMap的put方法,该方法不允许value重复
public V put(K key,V value)
{
//遍历所有value组成的集合
for (V val : valueSet() )
{
//如果指定value与试图放入集合的value相同
//则抛出一个RuntimeException异常
if (val.equals(value)
&& val.hashCode() == value.hashCode())
{
throw new RuntimeException
("MyMap实例中不允许有重复value!");
}
}
return super.put(key , value);
}
}