![xmpp协议的使用_第1页](http://file.renrendoc.com/FileRoot1/2018-8/12/539d2379-fd47-4a30-bf51-c7f37d223cb5/539d2379-fd47-4a30-bf51-c7f37d223cb51.gif)
![xmpp协议的使用_第2页](http://file.renrendoc.com/FileRoot1/2018-8/12/539d2379-fd47-4a30-bf51-c7f37d223cb5/539d2379-fd47-4a30-bf51-c7f37d223cb52.gif)
![xmpp协议的使用_第3页](http://file.renrendoc.com/FileRoot1/2018-8/12/539d2379-fd47-4a30-bf51-c7f37d223cb5/539d2379-fd47-4a30-bf51-c7f37d223cb53.gif)
![xmpp协议的使用_第4页](http://file.renrendoc.com/FileRoot1/2018-8/12/539d2379-fd47-4a30-bf51-c7f37d223cb5/539d2379-fd47-4a30-bf51-c7f37d223cb54.gif)
![xmpp协议的使用_第5页](http://file.renrendoc.com/FileRoot1/2018-8/12/539d2379-fd47-4a30-bf51-c7f37d223cb5/539d2379-fd47-4a30-bf51-c7f37d223cb55.gif)
已阅读5页,还剩41页未读, 继续免费阅读
版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
在 android 里面用的 smack 包其实叫做 asmack,该包提供了两种不同的连接方式: socket 和 httpclient。该并且提供了很多操作 xmpp 协议的 API,也方便各种不同自定 义协议的扩展。我们不需要自己重新去定义一套接收机制来扩展新的协议,只需继承然后 在类里处理自己的协议就可以了。而本文今天主要说两点,一点就是消息是如何接收的, 另一点就是消息是如何通知事件的。 总的思路 1.使用 socket 连接服务器 2.将 XmlPullParser 的数据源关联到 socket 的 InputStream 3.启动线程不断循环处理消息 4.将接收到的消息解析 xml 处理封装好成一个 Packet 包 5.将包广播给所有注册事件监听的类 逐步击破 (声明在看下面的文章时,最好先理解一下 smack 的使用,这样才能达到深入的理解) (谨记:上图只显示本文章解释所要用到的类和方法,减缩了一些跟本文主题无关的代码, 只留一条贯穿着从建立连接到接收消息的线。 ) 解析这块东西打算从最初的调用开始作为入口,抽丝剥茧,逐步揭开。 1. PacketListener packetListener = new PacketListener() Override public void processPacket(Packet packet) System.out .println(“Activity-processPacket“+ packet.toXML(); ; PacketFilter packetFilter = new PacketFilter() Override public booleanaccept(Packet packet) System.out.println(“Activity-accept“+ packet.toXML(); return true; ; 解释:创建包的监听以及包的过滤,当有消息到时就会广播到所有注册的监听,当然前提 是要通过 packetFilter 的过滤。 2. connection = new XMPPConnection(); XMPPConnection 在这构造函数里面主要配置 ip 地址和端口(super(new ConnectionConfiguration(“09“, 9991);) 3. connection.addPacketListener(packetListener, packetFilter); connection.connect(); 注册监听,开始初始化连接。 4. public void connect() / Stablishes the connection, readers and writers connectUsingConfiguration(config); 5. private void connectUsingConfiguration(ConnectionConfiguration config) String host = config.getHost(); intport = config.getPort(); try this.socket= newSocket(host, port); catch(UnknownHostException e) e.printStackTrace(); catch(IOException e) e.printStackTrace(); initConnection(); 通过之前设置的 ip 和端口,建立 socket 对象 6. protected void initDebugger() Class debuggerClass = null; try debuggerClass = Class.forName(“com.simualteSmack.ConsoleDebugger“); Constructor constructor = debuggerClass.getConstructor( Connection.class, Writer.class, Reader.class); debugger= (SmackDebugger) constructor.newInstance(this, writer, reader); reader= debugger.getReader(); catch(ClassNotFoundException e1) /TODOAuto-generated catch block e1.printStackTrace(); catch(Exception e) throw newIllegalArgumentException( “Cant initialize the configured debugger!“, e); private void initReaderAndWriter() try reader = newBufferedReader(newInputStreamReader(socket .getInputStream(), “UTF-8“); catch(UnsupportedEncodingException e) /TODOAuto-generated catch block e.printStackTrace(); catch(IOException e) /TODOAuto-generated catch block e.printStackTrace(); initDebugger(); private void initConnection() / Set the reader and writer instance variables initReaderAndWriter(); packetReader = new PacketReader(this); addPacketListener(debugger.getReaderListener(), null); / Start the packet reader. The startup() method will block until we / get an opening stream packet back from server. packetReader.startup(); 从三个方法可以看出,建立 reader 和 writer 的对象关联到 socket 的 InputStream, 实例化 ConsoleDebugger,该类主要是打印出接收到的消息,给 reader 设置了一个消 息的监听。接着建立 PacketReader 对象,并启动。PacketReader 主要负责消息的处理 和通知 7. public class PacketReader Private ExecutorService listenerExecutor; private boolean done; Private XMPPConnection connection; Private XmlPullParser parser; Private Thread readerThread; Protected PacketReader(finalXMPPConnection connection) this.connection= connection; this.init(); /* * Initializes the reader in order to be used. The reader is initialized * during the first connection and when reconnecting due to an abruptly * disconnection. */ protected void init() done= false; readerThread= newThread() public voidrun() parsePackets(this); ; readerThread.setName(“Smack Packet Reader “); readerThread.setDaemon(true); / create an executor to deliver incoming packets to listeners. / we will use a single thread with an unbounded queue. listenerExecutor= Executors .newSingleThreadExecutor(newThreadFactory() Override publicThread newThread(Runnable r) Thread thread = newThread(r, “smack listener processor“); thread.setDaemon(true); returnthread; ); resetParser(); /* * Starts the packet reader thread and returns once a connection to the * server has been established. A connection will be attempted for a maximum * of five seconds. An XMPPException will be thrown if the connection fails. * */ public voidstartup() readerThread.start(); /* * Shuts the packet reader down. */ public voidshutdown() done= true; / Shut down the listener executor. listenerExecutor.shutdown(); private voidresetParser() try parser= XmlPullParserFactory.newInstance().newPullParser(); parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); parser.setInput(connection.reader); catch(XmlPullParserException xppe) xppe.printStackTrace(); /* * Parse top-level packets in order to process them further. * *paramthread * the thread that is being used by the reader to parse incoming * packets. */ private void parsePackets(Thread thread) try Int eventType = parser.getEventType(); do if(eventType = XmlPullParser.START_TAG) if(parser.getName().equals(“message“) processPacket(PacketParserUtils.parseMessage(parser); System.out.println(“START_TAG“); else if(eventType = XmlPullParser.END_TAG) System.out.println(“END_TAG“); eventType = parser.next(); while(!done catch(Exception e) e.printStackTrace(); if(!done) private void processPacket(Packet packet) if(packet = null) return; / Loop through all collectors and notify the appropriate ones. for(PacketCollector collector : connection.getPacketCollectors() cessPacket(packet); / Deliver the incoming packet to listeners. listenerExecutor.submit(newListenerNotification(packet); /* * A runnable to notify all listeners of a packet. */ private class ListenerNotification implements Runnable Private Packet packet; Public ListenerNotification(Packet packet) this.packet= packet; public void run() for(ListenerWrapper listenerWrapper : connection.recvListeners .values() listenerWrapper.notifyListener(packet); 创建该类时就初始化线程和 ExecutorService ,接着调用 resetParser() 方法为 parser 设置输入源(这里是重点,parser 的数据都是通过这里获取) ,调用 startup 启动 线程,循环监听 parser,如果接收到消息根据消息协议的不同将调用 PacketParserUtils 类里的不同方法,这里调用 parseMessage()该方法主要处理 message 的消息,在该 方法里分析 message 消息并返回 packet 包。返回的包将调用 processPacket 方法,先 通知所有注册了 PacketCollector 的监听,接着消息 (listenerExecutor.submit(newListenerNotification(packet); )传递给所有注册 了 PacketListener 的监听。这样在 activity 开始之前注册的那个监听事件就会触发,从 而完成了整个流程。 7以上. 剩下的就是一些辅助包,很简单。比如 PacketCollector 这个类,它的用处主要用来处理 一些需要在发送后需要等待一个答复这样的请求。 protected synchronized void processPacket(Packet packet) System.out.println(“PacketCollector-processPacket“); if(packet = null) return; if(packetFilter= null| packetFilter.accept(packet) while(!resultQueue.offer(packet) resultQueue.poll(); Public Packet nextResult(longtimeout) longendTime = System.currentTimeMillis() + timeout; System.out.println(“nextResult“); do try returnresultQueue.poll(timeout, TimeUnit.MILLISECONDS); catch(InterruptedException e) /* ignore */ while(System.currentTimeMillis() entries = roster.getEntries(); for(RosterEntry entry : entries) System.out.print(entry.getName() + “ - “+ entry.getUser() + “ - “ + entry.getType() + “ - “+ entry.getGroups().size(); Presence presence = roster.getPresence(entry.getUser(); System.out.println(“ - “+ presence.getStatus() + “ - “ + presence.getFrom(); User user = newUser(); user.setName(entry.getName(); user.setUser(entry.getUser(); user.setType(entry.getType(); user.setSize(entry.getGroups().size(); user.setStatus(presence.getStatus(); user.setFrom(presence.getFrom(); userinfos.add(user); rosterAdapter.notifyDataSetChanged(); 单人聊天模块 第一次修改: 在主界面点击选择一个用户,进入聊天 Activity,ActivityChat 先获取传过来的用户,创 建聊天类并对该用户设置消息监听 ChatManager chatmanager = ConnectionSingleton.getInstance() .getChatManager(); / get user Intent intent = getIntent(); String user = intent.getStringExtra(“user“); System.out.println(“user:“+ user); / new a session newChat= chatmanager.createChat(user, null); / 监听聊天消息 chatmanager.addChatListener(newChatManagerListenerEx(); / send message try newChat.sendMessage(“im bird man“); catch(XMPPException e) /TODOAuto-generated catch block e.printStackTrace(); 监听类 public classChatManagerListenerEx implementsChatManagerListener Override public voidchatCreated(Chat chat, booleanarg1) /TODOAuto-generated method stub chat.addMessageListener(ml); public classMessageListenerEx implementsMessageListener Override public voidprocessMessage(Chat arg0, Message message) String result = message.getFrom() + “:“+ message.getBody(); System.out.println(result); android.os.Message msg = newandroid.os.Message(); msg.what= 0; Bundle bd = newBundle(); bd.putString(“msg“, result); msg.setData(bd); handler.sendMessage(msg); 所获取到的消息都是通过 handler 来更新 UI publicHandler handler= newHandler() Override public voidhandleMessage(android.os.Message msg) switch(msg.what) case0: String result = msg.getData().getString(“msg“); record.setText(record.getText() + “n“+ result); break; default: break; ; aaa 跟 bbb 的聊天 第二次修改: 第一次的测试,发现如果多个人之间都成为好友,那么他们之间的聊天就出现了接收不到 的信息,当然在跟 spark 测试时,spark 可以收到 android 端的信息,不过 android 客 户端是收到这个信息,不过却没有显示出来,具体原因还不太清楚。因此在第二次修改我 改成监听所有聊天信息包,然后再分析包的归属,分发到对应的聊天窗口。 这里就是监听到包后打印的消息,打印出了 jid 和消息内容 public classXmppMessageManager implementsChatManagerListener privateXMPPConnection _connection; privateChatManager manager = null; public voidinitialize(XMPPConnection connection) _connection = connection; manager = _connection.getChatManager(); manager.addChatListener(this); Override public voidchatCreated(Chat chat, booleanarg1) /TODOAuto-generated method stub chat.addMessageListener(newMessageListener() public voidprocessMessage(Chat newchat, Message message) / 若是聊天窗口存在,将消息转往目前窗口 / 若窗口不存在,创建新的窗口 System.out .println(message.getFrom() + “:“+ message.getBody(); if(!ActivityMain.chats.containsKey(message.getFrom() ActivityMain.chats.put(message.getFrom(), newchat); else ); 主要就是重写了 ChatManagerListener 类的监听,分发处理暂时没有想好怎么写。接着 在后台启动 service 就可以开始监听,行了第一次修改那些可以去掉了0。 多人聊天模块 也是在主界面的菜单进入 ActivityMultiChat,该界面显示所创建的房间,点击就跳转到 ActivityMultiRoom 。 获取所有房间比较简单,只需执行下面这段代码 hostrooms = MultiUserChat.getHostedRooms(ActivityMain.connection, “conference.zhanghaitao-pc“); 跳转到后获取要加入的房间的 jid,并创建监听。 jid = getIntent().getStringExtra(“jid“); /后面服务名称必需是创建房间的那个服务 String multiUserRoom = jid; try muc = newMultiUserChat(ActivityMain.connection, multiUserRoom); / 创建聊天室 ,进入房间后的 nickname muc.join(ActivityLogin.mCurrentAccount); Log.v(TAG, “join success“); catch(XMPPException e) /TODOAuto-generated catch block e.printStackTrace(); ChatPacketListener chatListener= newChatPacketListener(muc); muc.addMessageListener(chatListener); 监听大概的流程跟单人聊天差不多,都是 handler 来操作。不过多人聊天是重写了 PacketListener。具体如下(不过该方法是监听房间的信息,也就是说显示的是以房间为 名字的消息): classChatPacketListener implementsPacketListener privateString _number; privateDate _lastDate; privateMultiUserChat _muc; privateString _roomName; publicChatPacketListener(MultiUserChat muc) _number= “0“; _lastDate= newDate(0); _muc= muc; _roomName= muc.getRoom(); Override public voidprocessPacket(Packet packet) Message message = (Message) packet; String from = message.getFrom(); if(message.getBody() != null) DelayInformation inf = (DelayInformation) message.getExtension( “x“, “jabber:x:delay“); Date sentDate; if(inf != null) sentDate = inf.getStamp(); else sentDate = newDate(); Log.w(TAG, “Receive old message: date=“ + sentDate.toLocaleString() + “ ; message=“ + message.getBody(); android.os.Message msg = newandroid.os.Message(); msg.what= RECEIVE; Bundle bd = newBundle(); bd.putString(“from“, from); bd.putString(“body“, message.getBody(); msg.setData(bd); handler.sendMessage(msg); 下载模块 在主界面对着用户名长按,进入下载 activity。进入 activityFileTransfer,点击传输按 钮即可将文件传输给之前选择的用户,当然这里做得比较简单,并没有拒绝功能,一旦发 现有文件就接受。 FileTransferManager transfer =newFileTransferManager( ActivityMain.connection); String destination =user; OutgoingFileTransfer out = transfer .createOutgoingFileTransfer(destination +“/Smack“); 那用户是如何监听到有文件并且接受呢?在进入主界面的时候就已经开始了一个 service(fileListenerService) ,该服务创建文件的监听类(XmppFileManager),监听 类主要继承 FileTransferListener 重写里面的 fileTransferRequest 方法。 File saveTo; / set answerTo for replies and send() answerTo= request.getRequestor(); if(!Environment.MEDIA_MOUNTED.equals(Environment .getExternalStorageState() send(“External Media not mounted read/write“); return; else if(!landingDir.isDirectory() send(“The directory “+ landingDir.getAbsolutePath() + “ is not a directory“); return; saveTo = newFile(landingDir, request.getFileName(); if(saveTo.exists() send(“The file “+ saveTo.getAbsolutePath() + “ already exists“); / delete saveTo.delete(); / return; IncomingFileTransfer transfer = request.accept(); send(“File transfer: “+ saveTo.getName() + “ - “ + request.getFileSize() / 1024 + “ KB“); try transfer.recieveFile(saveTo); send(“File transfer: “+ saveTo.getName() + “ - “ + transfer.getStatus(); doublepercents = 0.0; while(!transfer.isDone() if(transfer.getStatus().equals(Status.in_progress) percents = (int) (transfer.getProgress() * 10000) / 100.0; send(“File transfer: “+ saveTo.getName() + “ - “ + percents + “%“); else if(transfer.getStatus().equals(Status.error) send(returnAndLogError(transfer); return; Thread.sleep(1000); if(transfer.getStatus().equals(Splete) send(“File transfer complete. File saved as “ + saveTo.getAbsolutePath(); else send(returnAndLogError(transfer); catch(Exception ex) String message = “Cannot receive the file because an error occured during the process.“ + ex; Log.e(TAG, message, ex); send(message); 网上说文件的传输遇到几个比较重要的问题,我也查看了很多资料(国内的基本是寥寥无 几,对此我感到挺无奈的,只能看国外,这样每次我的英语水平都提高了太无奈了0) 。 在这个 android asmack 的最新版本好像是解决了几个比较重要的问题,剩下一个传输文 件没反应的问题我在初始化连接时调用了 configure 方法解决。不过不知道是不是这个原 因,后面出现了一个神奇的问题,就是有时可以成功传输有时传输时客户端没响应(如果 有人完美解决了这个问题,那就赶紧将代码放出来一起共享-,以提高我国程序员 的整体水平,多伟大遥远的理想啊) 。 项目下载 /not-code/ASmack.zip XMPP 协议简介 XMPP 协议(Extensible Messaging and PresenceProtocol,可扩展消息处理现场协 议)是一种基于 XML 的协议,目的是为了解决及时通信标准而提出来的,最早是在 Jabber 上实现的。它继承了在 XML 环境中灵活的发展性。因此,基于 XMPP 的应用具有 超强的可扩展性。并且 XML 很易穿过防火墙,所以用 XMPP 构建的应用不易受到防火墙 的阻碍。利用 XMPP 作为通用的传输机制,不同组织内的不同应用都可以进行有效的通信。 这篇文章有基本的介绍, /xutaozero21/article/details/4873439 IM Instant Messenger,及时通信软件,就是大家使用的 QQ、MSN Messenger 和 Gtalk 等等。其中 Gtalk 就是基于 XMPP 协议的一个实现,其他的则不是。当前 IM 几乎作为 每个上网者必然使用的工具,在国外的大型企业中有一些企业级的 IM 应用,但是其商业 价值还没完全发挥出来。设想既然 XMPP 协议是一个公开的协议,那么每个企业都可以利 用它来开发适合本身企业工作,提高自身生产效率的 IM;甚至,你还可以在网络游戏中集 成这种通信软件,不但让你可以边游戏边聊天,也可以开发出适合游戏本身的 IM 应用, 比如说一些游戏关键场景提醒功能,团队语音交流等等都可以基于 IM 来实现。 本文主要讲解在 android 使用 xmpp 协议进行即时通信,所涉及3个主要的东西,它们是 openfire、smack 和 spark,这个三个东东结合起来就是完整的 xmpp IM 实现,这里简 单介绍一下这3个东东在下文的作用: openfire 主要是作为服务器,负责管理客户端的通信连接,以及提供客户端一些通信信息 和连接信息。 Smack 主要是 xmpp 协议的实现,提供了一套很好的 api,所以下面操作 xmpp 都是通 过使用 smack 的 api 来实现,当然因为是在 android 里,所以使用的是 asmack 这个包, 里面方法跟 smack 包差不多。 Spark 是 IM 客户端的实现,其实就是使用了 smack 的 api 实现的。 下图展示了三者之间的关系:(很明显这个图是偷别人的,具体是哪里我忘了,因为资料 都是复制到文档后慢慢研究看的) 从图上可以了解到,client 端和 server 端都可以通过插件的方式来进行扩展,smack 是 二者传递数据的媒介。 配置 openfire 服务器 具体步骤请移步: /blog/static/1766322992010111725339587/ 配置成功如果以后 ip 地址变了,那肯定又是开不了,解决办法请移步: /HappySheepherder/article/details/4707124 配置成功后,在服务器创建一个简单的用户来测试,然后安装 spark,设置好服务器的 ip 与端口,使用刚才创建的用户登录,登录 OK 说明服务器成功搭建。 Android IM 功能(因为是测试 demo,因此界面超级简陋,代码都是给出重要的一部分, 剩余的可以在最后下面项目查看) 配置要求 android 2.2、 asmack-jse.jar、myeclipse 连接服务器 在打开软件后会开始初始化,完成与 openfire 服务器的连接,设置一些配置 static XMPPConnection.DEBUG_ENABLED=true; finalConnectionConfiguration connectionConfig =newConnectionConfiguration( host, 5222,“); / Google talk / ConnectionConfiguration connectionConfig = new / ConnectionConfiguration( / ““, 5222, ““); / connectionConfig.setSASLAuthenticationEnabled(false); ActivityMain.connection=newXMPPConnection(connectionConfig); ActivityMain.connection.DEBUG_ENABLED=true; ProviderManager pm =ProviderManager.getInstance(); configure(pm); 注册模块 注册有两种方法:一种是用 createAccount ,不过我测试了一下发现不能创建用户,具 体原因不详,下面介绍第二种。 如上图:注册成功后服务器将多了 ggg 用户。 具体实现如下: Registration reg = newRegistration(); reg.setType(IQ.Type.SET); reg.setTo(ConnectionSingleton.getInstance().getServiceName(); reg.setUsername(username.getText().toString(); reg.setPassword(password.getText().toString(); reg.addAttribute(“android“, “geolo_createUser_android“); System.out.println(“reg:“+ reg); PacketFilter filter = newAndFilter(newPacketIDFilter(reg .getPacketID(), newPacketTypeFilter(IQ.class); PacketCollector collector = ConnectionSingleton.getInstance() .createPacketCollector(filter); ConnectionSingleton.getInstance().sendPacket(reg); result= (IQ) collector.nextResult(SmackConfiguration .getPacketReplyTimeout(); / Stop queuing results collector.cancel(); if(result= null) Toast.makeText(getApplicationContext(), “服, Toast.LENGTH_SHORT).show(); else if(result.getType() = IQ.Type.ERROR) if(result.getError().toString().equalsIgnoreCase( “conflict(409)“) Toast.makeText(getApplicationContext(), “这, Toast.LENGTH_SHORT).show(); else Toast.makeText(getApplicationContext(), “注, Toast.LENGTH_SHORT).show(); else if(result.getType() = IQ.Type.RESULT) Toast.makeText(getApplicationContext(), “恭, Toast.LENGTH_SHORT).show(); 使用注册类,设置好注册的用户名密码和一些属性字段,直接设置包过滤,根据这个过滤 创建一个结果集合,发送注册信息包,等待获取结果,剩余就是判断结果内容. 登录模块 登录比较简单 ConnectionSingleton.getInstance().connect();/ connect String account = etUsername.getText().toString(); String password = etPassword.getText().toString(); / 保存用户和密码 ActivityLogin.util.saveString(ACCOUNT_KEY, account); ActivityLogin.util.saveString(PASSWORD_KEY, password); ConnectionSingleton.getInstance().login(account, password);/ login / login success System.out.println(“login success“); ActivityLogin.mCurrentAccount= account; System.out.println(ConnectionSingleton.getInstance() .getUser(); / 登录成功后发现在线状态 Presence presence = newPresence(Presence.Type.available); ConnectionSingleton.getInstance().sendPacket(presence); / 开始主界面 Inten
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 二手房按揭买房买卖合同
- 国际公路运输合同范本
- 2025船舶买卖合同书样本版
- 提高创新能力的技能培训
- 提高人际关系的培训课程
- 品牌服务合同范本
- 2024年公共事业领域投资合同
- 吊车零租赁合同范本
- 钢钉铁钉售卖合同
- 2025有限责任公司银行贷款担保合同
- 职业健康监护评价报告编制指南
- 管理ABC-干嘉伟(美团网COO)
- 基于视觉的工业缺陷检测技术
- 军事英语词汇整理
- 家庭教育指导委员会章程
- DB31-T 1440-2023 临床研究中心建设与管理规范
- 老客户维护方案
- 高处作业安全教育培训讲义课件
- 万科物业管理公司全套制度(2016版)
- 动物检疫技术-动物检疫处理(动物防疫与检疫技术)
- 英语经典口语1000句
评论
0/150
提交评论