1. Socket
1.1. 知识点梳理复习
Socket01包: 单次对话
Socket02包:持久化通信 客户端可以循环键盘端输入,服务器循环读取,再转发 client.java ,server.java
Socket03包: 实际的群中,发送,和接收是2个相互独立的线程,client客户端:只能先发送,再读取,发送和读取都在main线程里,和实际情况不符,send,receive implements Runnable
现在的问题,启动多个客户端,仍旧只能谁先到,谁先聊 ,服务器只能支持和1个客户端通信 , Socket socket = server.accept();
While(true){
Socket socket = server.accept();
DataInputStream dis = new DataInputStream(socket.getInputStream);
DataOutputStream dos = new DataOutputStream(socket.getOutputStream);
While(true){
String msg = dis.readUTF();
dos.writeUTF(msg);
dos.flush();
}
}
这种解决方法,不可行,第一个客户端过来了,如果处在内层的死循环中不退出,即使有第2个客户端过来了,仍旧没有办法accept()到, 这种写法造成的还是谁先到,谁先聊
Socket04包: 将服务端开启多线程,ServerChannel多线程类(10086客服) ,来一个客户端,开启一个多线程类接待一下 serverChannel转发
Socket05包:群聊 ,1个客户端发送的,自身不收,其他的客户端能收到。客户端数量很多的时候,如何处理呢?如何取到这些客户端对应的客服(ServerChannel),利用这些客服发消息。
想到了解决办法,将客服类设计成内部类 ,在Server类声明的时候,声明1个集合List,专门用来存储客服(ServerChannel) ,转发给其他人实际上就是遍历集合
集合里数据何时存入呢? 接收到1个客户端,new1个多线程类对象 new1个ServerChannel对象后,这个时候就可以加入集合
Public class Server{
Private List<ServerChannel> list =new ArrayList<ServerChannel>();
Private class ServerChannel{
}
}
Socket06包: 加入私聊,支持人名 ,屏蔽关键词,保存聊天记录
1.2. Socket使用
见包socket06下的代码
关闭流 通用的工具类
package com.net.socket06;
import java.io.Closeable;
import java.io.IOException;
/**
* 通用的工具类,用来关闭流
* @author Administrator
*
*/
public class CloseUtil {
/**
* ... 用法和数组一样,既可以表示一个参数,也可以表示多个参数,甚至0个参数
* @param io
*/
public static void closeAll(Closeable... io){
for(Closeable temp:io){
if(null!=temp){
try {
temp.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
加载物理文件中的内容到集合中,写方法判断,是否包含集合中的关键词
package com.net.socket06;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* 读取文本文件中的内容,按行读取,保存在集合中
* 只读取1次 静态代码块
* @author Administrator
*/
public class WordsUtil {
private static List<String> wordsList = new ArrayList<String>();
static{
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader("E:/others/关键词.txt"));
String str = null;
while((str = br.readLine())!=null){
wordsList.add(str);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally{
CloseUtil.closeAll(br);
}
}
/**
* 判断是否包含集合中的关键词,如果包含返回true,否则返回false
* @param msg
* @return
*/
public static boolean isContain(String msg){
for(String s:WordsUtil.wordsList){
if(msg.contains(s)){
return true;
}
}
return false;
}
}
Server类服务端代码:
package com.net.socket06;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
/**
* 1.3个客户端
* 客户端1:
* 启动后 请输入姓名:jack
* 系统消息:jack,欢迎你进入。
* 系统消息:tom,已经进入了聊天室
* 系统消息:helen,已经进入了聊天室
* @helen: hello,你在做什么
* 今天吃了10个包子
* 客户端2:
* 请输入姓名 tom
* 系统消息:tom,欢迎你进入
* 系统消息:helen,已经进入了聊天室
* jack对大家说:今天吃了10个包子
* 客户端3:
* 请输入姓名 helen
* 系统消息:helen,欢迎你进入
* jack悄悄对你说: hello,你在做什么
* jack对大家说:今天吃了10个包子
* 支持私聊 如果发出的格式 @XXX: 只有 XXX能收到消息,其他人收不到
*
* 服务端:存储聊天记录 客户端 ip地址 + “ ”+客户端姓名: + “ ”+ 年-月-日 时:分:秒: 聊天内容 ,按行存储 ,存储到
* E:/others/聊天内容.txt中,追加
* 服务端: E:/others/关键词.txt, 里面有多行关键词,一行一个 ,“笨蛋”,“白痴”,“傻瓜”.....
* 如果发现要转发的内容中包含了关键词,这个时候,服务端提醒该发送者 :你发送的内容中有侮辱性词语,已被屏蔽。服务器不做转发。
*
* for(ServerChannel chan :clist){
* if(chan!=this){
* chan.sendMsg(msg);
* }
* }
*
* for(ServerChannel chan :clist){
* if(chan==this){
* continue;
* }
* chan.sendMsg(msg);
* }
*
* **
* 转发给其他人的方式2
*
private void sendToOthers(String msg,boolean isSys){
//将读取的内容转发给其他客户端
for(ServerChannel chan:clist){
//屏蔽自身,除了自身不发送
if(chan==this){
continue;
}
//chan :表示的是其他客户端对应的客服(多线程类ServerChannel)
if(isSys){ //系统消息
chan.sendMsg(msg);
}else{ //不是系统消息,正常的转发
chan.sendMsg(this.name+"对大家说: "+msg);
}
}
}
*
* 1.需要解决的 ,自身不能和自身私聊 ,提示用户
* 2.如果私聊的对象不存在,提示用户
* @author Administrator
*
*/
public class Server {
private List<ServerChannel> clist = new ArrayList<ServerChannel>();
public static void main(String[] args) throws IOException {
//对象名.方法名()
new Server().start();
}
public void start() throws IOException{
//创建服务端ServerSocket
ServerSocket server = new ServerSocket(8023);
while(true){
Socket socket = server.accept();
System.out.println("看, 有1个客户端过来了,开启一个客服接待一下,该客户端的ip地址:"+socket.getInetAddress().getHostAddress());
ServerChannel chan = new ServerChannel(socket);
//加入集合,方便后面的遍历
clist.add(chan);
new Thread(chan,"客服").start();
}
}
//设计成内部类,仍旧转发
public class ServerChannel implements Runnable{
private DataInputStream dis;
private DataOutputStream dos;
private Socket socket;
private boolean isRunning=true;
private String name; //用来存储检索出来的客户端的姓名
/**
* 通过带参构造传入socket对象,用于构建输入,输出流
* @param socket
*/
public ServerChannel(Socket socket) {
super();
this.socket = socket;
try {
dis = new DataInputStream(this.socket.getInputStream());
dos = new DataOutputStream(this.socket.getOutputStream());
//检索名字
this.name = dis.readUTF();
//给自身发送欢迎消息
this.sendMsg("系统消息:"+this.name+",欢迎你登录");
//给其他的已经登录的发送系统消息
this.sendToOthers("系统消息:"+this.name+"已经进入了聊天室",true);
} catch (IOException e) {
isRunning=false;
CloseUtil.closeAll(dos,dis);
e.printStackTrace();
}
}
/**
* 返回读取的内容
* @return
*/
private String getMsg(){
//读取内容
String msg=null;
try {
msg = dis.readUTF();
//存储到文本文件
saveIntoFile(msg,"E:/others/聊天内容.txt");
} catch (IOException e) {
isRunning =false;
clist.remove(this); //移除自身
CloseUtil.closeAll(dis);
e.printStackTrace();
}
return msg;
}
/**
* 将msg包装一下存储到path所指定的路径中
* @param msg 聊天的内容(服务器接收到的)
* @param path E:/others/聊天内容.txt
*/
private void saveIntoFile(String msg,String path){
//数据的格式: 客户端 ip地址 + “ ”+客户端姓名: + “ ”+ 年-月-日 时:分:秒: 聊天内容 ,按行存储
String data = this.socket.getInetAddress().getHostAddress()+" "+this.name+" "+DateUtil.getStrFromDate("yyyy-MM-dd HH:mm:ss", new Date())
+" :"+msg+"\r\n";
FileOutputStream fos = null;
try {
fos = new FileOutputStream(path,true);
fos.write(data.getBytes());
fos.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally{
CloseUtil.closeAll(fos);
}
}
/**
* 转发信息给其他人 @tom:afadfhello,aaaaa
* @param msg
* @isSys 该变量,用于表明是否是系统消息 ,如果是true 转发 “系统消息:XXX已经进入了聊天室” ,如果false 转发 "XXX对大家说:....."
*/
private void sendToOthers(String msg,boolean isSys){
//判断消息的类型,判断是否是私聊
if(msg.startsWith("@") && msg.indexOf(":")>-1){ //发给特定的人
//取出私聊对象的姓名
String toName = msg.substring(msg.indexOf("@")+1, msg.indexOf(":"));
//取出私聊内容
String toContent = msg.substring(msg.indexOf(":")+1);
System.out.println("****私聊对象="+toName+",私聊内容="+toContent);
//局部变量,声明1个boolean,用于表明,是否找到私聊对象 ,如果找到私聊对象,更改布尔值
boolean isFind = false;
//遍历集合,找到私聊对象所属的客服(ServerChannel) ,该客服给其对应的客户端发送消息
Iterator<ServerChannel> it = clist.iterator();
while(it.hasNext()){
ServerChannel chan = it.next();
if(chan.name.equals(toName) && !toName.equals(this.name)){
chan.sendMsg(this.name+"悄悄对你说: "+toContent);
isFind=true;
}
}
//循环结束后,再次判断isFind的值
if(!isFind){
if(toName.equals(this.name)){
this.sendMsg(this.name+"你不能和自身私聊"); //给自身发送提示信息
}else{
this.sendMsg(this.name+",你要私聊的对象“"+toName+"”不存在,无法私聊");
}
}
}else{ //正常的发送给其他客户端
//将读取的内容转发给其他客户端
for(ServerChannel chan:clist){
//屏蔽自身,除了自身不发送
if(chan!=this){
//chan :表示的是其他客户端对应的客服(多线程类ServerChannel)
if(isSys){ //系统消息
chan.sendMsg(msg);
}else{ //不是系统消息,正常的转发
chan.sendMsg(this.name+"对大家说: "+msg);
}
}
}
}
}
/**
* 发送给自身对应的客户端
* @param msg
*/
private void sendMsg(String msg){
try {
dos.writeUTF(msg);
dos.flush();
} catch (IOException e) {
isRunning =false;
clist.remove(this); //移除自身
CloseUtil.closeAll(dos);
e.printStackTrace();
}
}
@Override
public void run() {
while(isRunning){
String msg = getMsg();
if(null!=msg && msg.trim().length()!=0){
if(WordsUtil.isContain(msg)){
sendMsg(this.name+"你发送的内容包含侮辱性词语,已被屏蔽");
}else{
sendToOthers(msg,false);
}
}else{
sendMsg(this.name+"你发送的内容不能为空");
}
}
}
}
}
客户端Client代码
package com.njwb.net.socket06;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
import java.net.UnknownHostException;
/**
*
* @author Administrator
*
*/
public class Client {
public static void main(String[] args) throws UnknownHostException, IOException {
BufferedReader console = new BufferedReader(new InputStreamReader(System.in));
System.out.println("请输入姓名:");
String name = console.readLine();
//创建Socket对象,同时指定要连接的服务端的ip地址和端口号
Socket client = new Socket("127.0.0.1",8023);
//开启发送线程
new Thread(new Send(client,name),"发送线程").start();
//开启接收线程
new Thread(new Receive(client),"接收线程").start();
}
}
客户端发送线程 send.java
package com.njwb.net.socket06;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
/**
* 发送线程
* @author Administrator
*
*/
public class Send implements Runnable {
private DataOutputStream dos; //声明输出流对象,用于发送消息
private BufferedReader console; //声明键盘端输入的实例变量
private Socket socket; //声明socket,用于构建输出流对象
private boolean isRunning=true; //用于控制循环是否继续
private String name; //用于保存客户端的姓名
/**
* 往线程传递数据,此处从client类传入socket对象,用于构建输出流
* @param socket
*/
public Send(Socket socket,String name) {
super();
this.socket = socket;
this.name = name;
console = new BufferedReader(new InputStreamReader(System.in));
try {
//根据传入的socket,构建了输出流对象
dos = new DataOutputStream(this.socket.getOutputStream());
//发送名字
dos.writeUTF(this.name);
dos.flush();
} catch (IOException e) {
isRunning=false;
CloseUtil.closeAll(dos,console);
e.printStackTrace();
}
}
/**
* 返回的键盘端输入的字符串
* @return
*/
private String getMsg(){
String msg="";
try {
msg = console.readLine();
} catch (IOException e) {
isRunning=false;
CloseUtil.closeAll(console);
e.printStackTrace();
}
return msg;
}
/**
* 发送信息
* @param msg
*/
private void sendMsg(String msg){
if(!StringUtil.isEmpty(msg)){ //发送的消息做非空验证
try {
dos.writeUTF(msg);
dos.flush();
} catch (IOException e) {
isRunning=false;
CloseUtil.closeAll(dos);
e.printStackTrace();
}
}
}
@Override
public void run() {
while(isRunning){
sendMsg(getMsg());
}
}
}
客户端接收线程 Receive.java
package com.njwb.net.socket06;
import java.io.DataInputStream;
import java.io.IOException;
import java.net.Socket;
/**
* 接收线程
* @author Administrator
*
*/
public class Receive implements Runnable{
private DataInputStream dis; //声明输入流对象,用于读取服务端发送过来的
private Socket socket; //声明socket对象
private boolean isRunning=true; //声明boolean变量,用于控制循环是否继续
public Receive(Socket socket) {
super();
this.socket = socket;
//根据传入的socket对象,构建输入流对象
try {
dis = new DataInputStream(this.socket.getInputStream());
} catch (IOException e) {
isRunning=false;
CloseUtil.closeAll(dis);
e.printStackTrace();
}
}
/**
* 返回接收的字符串
* @return
*/
private String getMsg(){
String msg=null;
try {
msg = dis.readUTF();
} catch (IOException e) {
isRunning=false;
CloseUtil.closeAll(dis);
e.printStackTrace();
}
return msg;
}
@Override
public void run() {
while(isRunning){
System.out.println(getMsg());
}
}
}
1.3. UDP通信
l 基于UDP协议的通信方式,称为数据报通信方式
每个数据发送单元被统一封装成数据包的方式,发送方将数据包发送到网络中,数据包在网络中去寻找他的目的地。
l 以数据为中心,非面向连接,不安全,数据可能丢失,效率高。
l DatagramSocket:用于发送或接收数据包
l DatagramPacket:数据容器(封包)的作用
l 基本步骤:
1.创建客户端的DatagramSocket,创建时,定义客户端的监听端口。
2.创建服务器端的DatagramSocket,创建时,定义服务器端的监听端口
3.在客户端定义DatagramPacket对象,封装待发送的数据包。
4.客户端将数据包发送出去
5.服务端接收数据包
发送引用数据类型
l 客户端:
1) 创建客户端 DatagramSocket类 +指定端口
2) 准备数据 字节数组
3) 打包 DatagramPacket +服务器地址及端口
4) 发送
5) 释放资源
l 服务器:
1) 创建服务端DatagramSocket类+指定端口
2) 准备接受容器 字节数组 封装DatagramPacket(封装成包)
3) 包 接收数据
4) 分析
5) 释放资源
发送基本数据类型
l 客户端:
1) 创建客户端 DatagramSocket类 +指定端口
2) 准备数据 基本数据类型转换成字节数组(字节数组输出流ByteArrayOutputStream toByteArray 数据字节输出流DataOutputStream)
3) 打包 DatagramPacket +服务器地址及端口(发送的地点以及端口)
4) 发送
5) 释放资源
l 服务器:
1) 创建服务端DatagramSocket类+指定端口
2) 准备接受容器 字节数组 封装DatagramPacket(封装成包)
3) 包 接收数据
4) 分析数据 字节数组转换成基本数据类型(字节数组输入流ByteArrayOutputStream 数据字节输入流DataInputStream)
5) 释放资源
例题1:对应的需求1
package com.njwb.net.udp;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
/**
* 只需要记住2个类DatagramSocket DatagramPacket
* DatagramSocket(int port)
创建数据报套接字并将其绑定到本地主机上的指定端口。
DatagramPacket(byte[] buf, int length)
构造 DatagramPacket,用来接收长度为 length 的数据包。 服务端
* 需求:发送“今天吃了10个包子” 发送给服务器
* 需求:发送 139.87 double基本数据类型发送给服务器
* 1)创建服务端DatagramSocket类+指定端口
2)准备接受容器 字节数组 封装DatagramPacket(封装成包)
3)包 接收数据
4)分析
5)释放资源
* @author Administrator
*
*/
public class Server {
public static void main(String[] args) throws IOException {
//1.创建服务端DatagramSocket类+指定端口
DatagramSocket server = new DatagramSocket(8888);
//2.准备接受容器 字节数组
byte[] container = new byte[1024];
//3.封装DatagramPacket(封装成包)
DatagramPacket packet = new DatagramPacket(container,container.length);
//4.包 接收数据(接收的数据存储在packet中)
server.receive(packet);
//5.分析 (数据在packet中,需要取出,输出在控制台)
byte[] data = packet.getData();
int len = packet.getLength();
//转换成字符串打印输出
System.out.println(new String(data,0,len));
//6.释放资源
server.close();
}
}
package com.net.udp;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
/**
* DatagramSocket(int port)
创建数据报套接字并将其绑定到本地主机上的指定端口。
(要发送的数据对应的byte数组,byte数组的长度,InetAddress.getByName("服务器的ip地址"),服务器的端口号)
DatagramPacket(byte[] buf, int length, InetAddress address, int port) 你熟悉一点
构造数据报包,用来将长度为 length 的包发送到指定主机上的指定端口号。
(要发送的数据对应的byte数组,byte数组的长度, SocketAddress的子类对象)
DatagramPacket(byte[] buf, int length, SocketAddress address)
SocketAddress抽象类,不能直接new,此处放置的是SocketAddress的子类 InetSocketAddress ,
InetSocketAddress(String hostname, int port)
根据主机名和端口号创建套接字地址。 (服务器的ip地址,服务器的端口号)
构造数据报包,用来将长度为 length 的包发送到指定主机上的指定端口号。
* 客户端:
* 1)创建客户端 DatagramSocket类 +指定端口
2)准备数据 字节数组
3)打包 DatagramPacket +服务器地址及端口
4)发送
5)释放资源
* @author Administrator
*
*/
public class Client {
public static void main(String[] args) throws IOException {
//1.创建客户端 DatagramSocket类 +指定端口 ,不要和服务器相同
DatagramSocket client = new DatagramSocket(7777);
//2.准备数据,要将数据转换成字节数组
String str = "今天吃了10个包子";
byte[] data = str.getBytes();
//3.打包 DatagramPacket +服务器地址及端口
DatagramPacket packet = new DatagramPacket(data,data.length,InetAddress.getByName("127.0.0.1"),8888);
//DatagramPacket packet = new DatagramPacket(data,data.length,new InetSocketAddress("127.0.0.1",8888));
//4.发送
client.send(packet);
//5.释放资源
client.close();
}
}
例题2:
package com.net.udp;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import com.njwb.net.socket06.CloseUtil;
/**
* 接收的基本数据类型
* 1) 创建服务端DatagramSocket类+指定端口
2) 准备接受容器 字节数组 封装DatagramPacket(封装成包)
3) 包 接收数据
4) 分析数据 字节数组转换成基本数据类型(字节数组输入流ByteArrayOutputStream 数据字节输入流DataInputStream)
5) 释放资源
* @author Administrator
*
*/
public class Server2 {
public static void main(String[] args) throws IOException {
//1.创建服务端DatagramSocket类+指定端口
DatagramSocket server = new DatagramSocket(8888);
//2.准备接受容器 字节数组
byte[] container = new byte[1024];
//3.封装DatagramPacket(封装成包)
DatagramPacket packet = new DatagramPacket(container,container.length);
//4.包 接收数据(接收的数据存储在packet中)
server.receive(packet);
//5.分析 (数据在packet中,需要取出,输出在控制台)
byte[] data = packet.getData();
//将byte[]数组转换成基本数据类型
double num = convert(data);
System.out.println("num="+num);
//6.释放资源
server.close();
}
/**
* 字节数组转换成double返回
* @param data
* @return
* @throws IOException
*/
public static double convert(byte[] data) throws IOException{
ByteArrayInputStream bis = new ByteArrayInputStream(data);
DataInputStream dis = new DataInputStream(bis);
double num = dis.readDouble();
CloseUtil.closeAll(dis,bis);
return num;
}
}
package com.njwb.net.udp;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import com.njwb.net.socket06.CloseUtil;
/**
* 发送的基本数据类型 double
* 1) 创建客户端 DatagramSocket类 +指定端口
2) 准备数据 基本数据类型转换成字节数组(字节数组输出流ByteArrayOutputStream toByteArray 数据字节输出流DataOutputStream)
3) 打包 DatagramPacket +服务器地址及端口(发送的地点以及端口)
4) 发送
5) 释放资源
* @author Administrator
*
*/
public class Client2 {
public static void main(String[] args) throws IOException {
//1.创建客户端 DatagramSocket类 +指定端口 ,不要和服务器相同
DatagramSocket client = new DatagramSocket(7777);
//2.准备数据,要将数据转换成字节数组
double num = 139.87;
// byte[] data = (num+"").getBytes();
byte[] data = convert(num);
//3.打包 DatagramPacket +服务器地址及端口
DatagramPacket packet = new DatagramPacket(data,data.length,InetAddress.getByName("127.0.0.1"),8888);
// DatagramPacket packet = new DatagramPacket(data,data.length,new InetSocketAddress("127.0.0.1",8888));
//4.发送
client.send(packet);
//5.释放资源
client.close();
}
/**
* 将传入的double类型的数字,转换成byte[]数组返回 ,整个方法作用 : byte[] data = (num+"").getBytes();
* @param num
* @return
* @throws IOException
*/
public static byte[] convert(double num) throws IOException{
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
//将基本数据类型写出到内存中
dos.writeDouble(num);
dos.flush();
//将字节数组输出流转换成byte[]数组
byte[] data = bos.toByteArray();
CloseUtil.closeAll(dos,bos);
return data;
}
}
1.4. 作业
l 精通单线程调试,熟练多线程调试。
l 熟悉掌握Socket
l 复习之前的知识点