今天跟大家分享一下我那QQ小项目中服务器与客户端的核心代码,并谈谈一些我的建议和看法,希望大家多多支持,你们的支持,就是我继续分享的动力,哈哈!

一、服务器,好了,废话不多说,我们先来看看服务器部分,我这里用到线程池,至于为什么用线程池,不知道的童鞋可以去我的另一篇blog看看:http://blog.csdn.net/weidi1989/article/details/7930820。当一个用户连接上之后,我们马上将该用户的socket丢入已经建好的线程池中去处理,这样可以很快腾出时间来接受下一个用户的连接,而线程池中的这个线程又分支为两个线程,一个是读消息线程,一个是写消息线程,当然,因为我这个聊天是用来转发消息的,所以还以单例模式建了一个Map用来存放每个用户的写消息线程(如果用户多的话,这是相当消耗资源的),以便在转发消息的时候,通过Map的key就可以取出对应用户的写消息线程,从而达到转发消息的目的。具体下面再说

[java]  view plain copy
  1. /**
  2. * 服务器,接受用户登录、离线、转发消息
  3. *
  4. * @author way
  5. *
  6. */
  7. public class Server {
  8. private ExecutorService executorService;// 线程池
  9. private ServerSocket serverSocket = null;
  10. private Socket socket = null;
  11. private boolean isStarted = true;//是否循环等待
  12. public Server() {
  13. try {
  14. // 创建线程池,池中具有(cpu个数*50)条线程
  15. executorService = Executors.newFixedThreadPool(Runtime.getRuntime()
  16. .availableProcessors() * 50);
  17. serverSocket = new ServerSocket(Constants.SERVER_PORT);
  18. } catch (IOException e) {
  19. e.printStackTrace();
  20. quit();
  21. }
  22. }
  23. public void start() {
  24. System.out.println(MyDate.getDateCN() + " 服务器已启动...");
  25. try {
  26. while (isStarted) {
  27. socket = serverSocket.accept();
  28. String ip = socket.getInetAddress().toString();
  29. System.out.println(MyDate.getDateCN() + " 用户:" + ip + " 已建立连接");
  30. // 为支持多用户并发访问,采用线程池管理每一个用户的连接请求
  31. if (socket.isConnected())
  32. executorService.execute(new SocketTask(socket));// 添加到线程池
  33. }
  34. if (socket != null)//循环结束后,记得关闭socket,释放资源
  35. socket.close();
  36. if (serverSocket != null)
  37. serverSocket.close();
  38. } catch (IOException e) {
  39. e.printStackTrace();
  40. // isStarted = false;
  41. }
  42. }
  43. private final class SocketTask implements Runnable {
  44. private Socket socket = null;
  45. private InputThread in;
  46. private OutputThread out;
  47. private OutputThreadMap map;
  48. public SocketTask(Socket socket) {
  49. this.socket = socket;
  50. map = OutputThreadMap.getInstance();
  51. }
  52. @Override
  53. public void run() {
  54. out = new OutputThread(socket, map);//
  55. // 先实例化写消息线程,(把对应用户的写线程存入map缓存器中)
  56. in = new InputThread(socket, out, map);// 再实例化读消息线程
  57. out.setStart(true);
  58. in.setStart(true);
  59. in.start();
  60. out.start();
  61. }
  62. }
  63. /**
  64. * 退出
  65. */
  66. public void quit() {
  67. try {
  68. this.isStarted = false;
  69. serverSocket.close();
  70. } catch (IOException e) {
  71. e.printStackTrace();
  72. }
  73. }
  74. public static void main(String[] args) {
  75. new Server().start();
  76. }
  77. }

二、服务器写消息线程,接下来,我们来看看写消息线程,很简单的一段代码,有注释,我就不多说了:

[java]  view plain copy
  1. /**
  2. * 写消息线程
  3. *
  4. * @author way
  5. *
  6. */
  7. public class OutputThread extends Thread {
  8. private OutputThreadMap map;
  9. private ObjectOutputStream oos;
  10. private TranObject object;
  11. private boolean isStart = true;// 循环标志位
  12. private Socket socket;
  13. public OutputThread(Socket socket, OutputThreadMap map) {
  14. try {
  15. this.socket = socket;
  16. this.map = map;
  17. oos = new ObjectOutputStream(socket.getOutputStream());// 在构造器里面实例化对象输出流
  18. } catch (IOException e) {
  19. e.printStackTrace();
  20. }
  21. }
  22. public void setStart(boolean isStart) {//用于外部关闭写线程
  23. this.isStart = isStart;
  24. }
  25. // 调用写消息线程,设置了消息之后,唤醒run方法,可以节约资源
  26. public void setMessage(TranObject object) {
  27. this.object = object;
  28. synchronized (this) {
  29. notify();
  30. }
  31. }
  32. @Override
  33. public void run() {
  34. try {
  35. while (isStart) {
  36. // 没有消息写出的时候,线程等待
  37. synchronized (this) {
  38. wait();
  39. }
  40. if (object != null) {
  41. oos.writeObject(object);
  42. oos.flush();
  43. }
  44. }
  45. if (oos != null)// 循环结束后,关闭流,释放资源
  46. oos.close();
  47. if (socket != null)
  48. socket.close();
  49. } catch (InterruptedException e) {
  50. e.printStackTrace();
  51. } catch (IOException e) {
  52. e.printStackTrace();
  53. }
  54. }
  55. }

三、服务器写消息线程缓存器,接下来让我们看一下那个写消息线程缓存器的庐山真面目:

[java]  view plain copy
  1. /**
  2. * 存放写线程的缓存器
  3. *
  4. * @author way
  5. */
  6. public class OutputThreadMap {
  7. private HashMap<Integer, OutputThread> map;
  8. private static OutputThreadMap instance;
  9. // 私有构造器,防止被外面实例化改对像
  10. private OutputThreadMap() {
  11. map = new HashMap<Integer, OutputThread>();
  12. }
  13. // 单例模式像外面提供该对象
  14. public synchronized static OutputThreadMap getInstance() {
  15. if (instance == null) {
  16. instance = new OutputThreadMap();
  17. }
  18. return instance;
  19. }
  20. // 添加写线程的方法
  21. public synchronized void add(Integer id, OutputThread out) {
  22. map.put(id, out);
  23. }
  24. // 移除写线程的方法
  25. public synchronized void remove(Integer id) {
  26. map.remove(id);
  27. }
  28. // 取出写线程的方法,群聊的话,可以遍历取出对应写线程
  29. public synchronized OutputThread getById(Integer id) {
  30. return map.get(id);
  31. }
  32. // 得到所有写线程方法,用于向所有在线用户发送广播
  33. public synchronized List<OutputThread> getAll() {
  34. List<OutputThread> list = new ArrayList<OutputThread>();
  35. for (Map.Entry<Integer, OutputThread> entry : map.entrySet()) {
  36. list.add(entry.getValue());
  37. }
  38. return list;
  39. }
  40. }

四、服务器读消息线程,接下来是读消息线程,这里包括两个部分,一部分是读消息,另一部分是处理消息,我以分开的形式贴出代码,虽然我是写在一个类里面的:

[java]  view plain copy
  1. /**
  2. * 读消息线程和处理方法
  3. *
  4. * @author way
  5. *
  6. */
  7. public class InputThread extends Thread {
  8. private Socket socket;// socket对象
  9. private OutputThread out;// 传递进来的写消息线程,因为我们要给用户回复消息啊
  10. private OutputThreadMap map;//写消息线程缓存器
  11. private ObjectInputStream ois;//对象输入流
  12. private boolean isStart = true;//是否循环读消息
  13. public InputThread(Socket socket, OutputThread out, OutputThreadMap map) {
  14. this.socket = socket;
  15. this.out = out;
  16. this.map = map;
  17. try {
  18. ois = new ObjectInputStream(socket.getInputStream());//实例化对象输入流
  19. } catch (IOException e) {
  20. e.printStackTrace();
  21. }
  22. }
  23. public void setStart(boolean isStart) {//提供接口给外部关闭读消息线程
  24. this.isStart = isStart;
  25. }
  26. @Override
  27. public void run() {
  28. try {
  29. while (isStart) {
  30. // 读取消息
  31. readMessage();
  32. }
  33. if (ois != null)
  34. ois.close();
  35. if (socket != null)
  36. socket.close();
  37. } catch (ClassNotFoundException e) {
  38. e.printStackTrace();
  39. } catch (IOException e) {
  40. e.printStackTrace();
  41. }
  42. }

五、服务器消息处理,下面是处理消息的方法,由于比较麻烦以及各种纠结,我就与读消息线程分开贴,显得稍微简洁一点:

[java]  view plain copy
  1. /**
  2. * 读消息以及处理消息,抛出异常
  3. *
  4. * @throws IOException
  5. * @throws ClassNotFoundException
  6. */
  7. public void readMessage() throws IOException, ClassNotFoundException {
  8. Object readObject = ois.readObject();// 从流中读取对象
  9. UserDao dao = UserDaoFactory.getInstance();// 通过dao模式管理后台
  10. if (readObject != null && readObject instanceof TranObject) {
  11. TranObject read_tranObject = (TranObject) readObject;// 转换成传输对象
  12. switch (read_tranObject.getType()) {
  13. case REGISTER:// 如果用户是注册
  14. User registerUser = (User) read_tranObject.getObject();
  15. int registerResult = dao.register(registerUser);
  16. System.out.println(MyDate.getDateCN() + " 新用户注册:"
  17. + registerResult);
  18. // 给用户回复消息
  19. TranObject<User> register2TranObject = new TranObject<User>(
  20. TranObjectType.REGISTER);
  21. User register2user = new User();
  22. register2user.setId(registerResult);
  23. register2TranObject.setObject(register2user);
  24. out.setMessage(register2TranObject);
  25. break;
  26. case LOGIN:
  27. User loginUser = (User) read_tranObject.getObject();
  28. ArrayList<User> list = dao.login(loginUser);
  29. TranObject<ArrayList<User>> login2Object = new TranObject<ArrayList<User>>(
  30. TranObjectType.LOGIN);
  31. if (list != null) {// 如果登录成功
  32. TranObject<User> onObject = new TranObject<User>(
  33. TranObjectType.LOGIN);
  34. User login2User = new User();
  35. login2User.setId(loginUser.getId());
  36. onObject.setObject(login2User);
  37. for (OutputThread onOut : map.getAll()) {
  38. onOut.setMessage(onObject);// 广播一下用户上线
  39. }
  40. map.add(loginUser.getId(), out);// 先广播,再把对应用户id的写线程存入map中,以便转发消息时调用
  41. login2Object.setObject(list);// 把好友列表加入回复的对象中
  42. } else {
  43. login2Object.setObject(null);
  44. }
  45. out.setMessage(login2Object);// 同时把登录信息回复给用户
  46. System.out.println(MyDate.getDateCN() + " 用户:"
  47. + loginUser.getId() + " 上线了");
  48. break;
  49. case LOGOUT:// 如果是退出,更新数据库在线状态,同时群发告诉所有在线用户
  50. User logoutUser = (User) read_tranObject.getObject();
  51. int offId = logoutUser.getId();
  52. System.out
  53. .println(MyDate.getDateCN() + " 用户:" + offId + " 下线了");
  54. dao.logout(offId);
  55. isStart = false;// 结束自己的读循环
  56. map.remove(offId);// 从缓存的线程中移除
  57. out.setMessage(null);// 先要设置一个空消息去唤醒写线程
  58. out.setStart(false);// 再结束写线程循环
  59. TranObject<User> offObject = new TranObject<User>(
  60. TranObjectType.LOGOUT);
  61. User logout2User = new User();
  62. logout2User.setId(logoutUser.getId());
  63. offObject.setObject(logout2User);
  64. for (OutputThread offOut : map.getAll()) {// 广播用户下线消息
  65. offOut.setMessage(offObject);
  66. }
  67. break;
  68. case MESSAGE:// 如果是转发消息(可添加群发)
  69. // 获取消息中要转发的对象id,然后获取缓存的该对象的写线程
  70. int id2 = read_tranObject.getToUser();
  71. OutputThread toOut = map.getById(id2);
  72. if (toOut != null) {// 如果用户在线
  73. toOut.setMessage(read_tranObject);
  74. } else {// 如果为空,说明用户已经下线,回复用户
  75. TextMessage text = new TextMessage();
  76. text.setMessage("亲!对方不在线哦,您的消息将暂时保存在服务器");
  77. TranObject<TextMessage> offText = new TranObject<TextMessage>(
  78. TranObjectType.MESSAGE);
  79. offText.setObject(text);
  80. offText.setFromUser(0);
  81. out.setMessage(offText);
  82. }
  83. break;
  84. case REFRESH:
  85. List<User> refreshList = dao.refresh(read_tranObject
  86. .getFromUser());
  87. TranObject<List<User>> refreshO = new TranObject<List<User>>(
  88. TranObjectType.REFRESH);
  89. refreshO.setObject(refreshList);
  90. out.setMessage(refreshO);
  91. break;
  92. default:
  93. break;
  94. }
  95. }
  96. }

好了,服务器的核心代码就这么一些了,很简单吧?是的,因为我们还有很多事情没有去做,比如说心跳监测用户是否一直在线,如果不在线,就释放资源等,这些都是商业项目中必须要考虑到的问题,至于这个通过心跳监测用户是否在线,我说说我的一些想法吧:由客户端定时给服务器发送一个心跳包(最好是空包,节约流量),服务器也定时去监测那个心跳包,如果有3次未收到客户端的心跳包,就判断该用户已经掉线,释放资源,至于这次数和时间间隔,就随情况而定了。如果有什么更好的其他建议,欢迎给我留言,谢谢。

六、消息传输对象,下面,我们来看看,这个超级消息对象和定义好的消息类型:

[java]  view plain copy
  1. /**
  2. * 传输的对象,直接通过Socket传输的最大对象
  3. *
  4. * @author way
  5. */
  6. public class TranObject<T> implements Serializable {
  7. /**
  8. *
  9. */
  10. private static final long serialVersionUID = 1L;
  11. private TranObjectType type;// 发送的消息类型
  12. private int fromUser;// 来自哪个用户
  13. private int toUser;// 发往哪个用户
  14. private T object;// 传输的对象,这个对象我们可以自定义任何
  15. private List<Integer> group;// 群发给哪些用户
  16. get...set...
[java]  view plain copy
  1. /**
  2. * 传输对象类型
  3. *
  4. * @author way
  5. *
  6. */
  7. public enum TranObjectType {
  8. REGISTER, // 注册
  9. LOGIN, // 用户登录
  10. LOGOUT, // 用户退出登录
  11. FRIENDLOGIN, // 好友上线
  12. FRIENDLOGOUT, // 好友下线
  13. MESSAGE, // 用户发送消息
  14. UNCONNECTED, // 无法连接
  15. FILE, // 传输文件
  16. REFRESH,//刷新好友列表
  17. }

七、客户端,然后是客户端部分了,其实跟服务器差不多,只是没有建立线程池了,因为没有必要,是吧?然后实例化写线程和读线程没有先后顺序,这也勉强算一个区别吧~呵呵

[java]  view plain copy
  1. /**
  2. * 客户端
  3. *
  4. * @author way
  5. *
  6. */
  7. public class Client {
  8. private Socket client;
  9. private ClientThread clientThread;
  10. private String ip;
  11. private int port;
  12. public Client(String ip, int port) {
  13. this.ip = ip;
  14. this.port = port;
  15. }
  16. public boolean start() {
  17. try {
  18. client = new Socket();
  19. // client.connect(new InetSocketAddress(Constants.SERVER_IP,
  20. // Constants.SERVER_PORT), 3000);
  21. client.connect(new InetSocketAddress(ip, port), 3000);
  22. if (client.isConnected()) {
  23. // System.out.println("Connected..");
  24. clientThread = new ClientThread(client);
  25. clientThread.start();
  26. }
  27. } catch (IOException e) {
  28. e.printStackTrace();
  29. return false;
  30. }
  31. return true;
  32. }
  33. // 直接通过client得到读线程
  34. public ClientInputThread getClientInputThread() {
  35. return clientThread.getIn();
  36. }
  37. // 直接通过client得到写线程
  38. public ClientOutputThread getClientOutputThread() {
  39. return clientThread.getOut();
  40. }
  41. // 直接通过client停止读写消息
  42. public void setIsStart(boolean isStart) {
  43. clientThread.getIn().setStart(isStart);
  44. clientThread.getOut().setStart(isStart);
  45. }
  46. public class ClientThread extends Thread {
  47. private ClientInputThread in;
  48. private ClientOutputThread out;
  49. public ClientThread(Socket socket) {
  50. in = new ClientInputThread(socket);
  51. out = new ClientOutputThread(socket);
  52. }
  53. public void run() {
  54. in.setStart(true);
  55. out.setStart(true);
  56. in.start();
  57. out.start();
  58. }
  59. // 得到读消息线程
  60. public ClientInputThread getIn() {
  61. return in;
  62. }
  63. // 得到写消息线程
  64. public ClientOutputThread getOut() {
  65. return out;
  66. }
  67. }
  68. }

八、客户端写消息线程,先看看客户端写消息线程吧:

[java]  view plain copy
  1. /**
  2. * 客户端写消息线程
  3. *
  4. * @author way
  5. *
  6. */
  7. public class ClientOutputThread extends Thread {
  8. private Socket socket;
  9. private ObjectOutputStream oos;
  10. private boolean isStart = true;
  11. private TranObject msg;
  12. public ClientOutputThread(Socket socket) {
  13. this.socket = socket;
  14. try {
  15. oos = new ObjectOutputStream(socket.getOutputStream());
  16. } catch (IOException e) {
  17. e.printStackTrace();
  18. }
  19. }
  20. public void setStart(boolean isStart) {
  21. this.isStart = isStart;
  22. }
  23. // 这里处理跟服务器是一样的
  24. public void setMsg(TranObject msg) {
  25. this.msg = msg;
  26. synchronized (this) {
  27. notify();
  28. }
  29. }
  30. @Override
  31. public void run() {
  32. try {
  33. while (isStart) {
  34. if (msg != null) {
  35. oos.writeObject(msg);
  36. oos.flush();
  37. if (msg.getType() == TranObjectType.LOGOUT) {// 如果是发送下线的消息,就直接跳出循环
  38. break;
  39. }
  40. synchronized (this) {
  41. wait();// 发送完消息后,线程进入等待状态
  42. }
  43. }
  44. }
  45. oos.close();// 循环结束后,关闭输出流和socket
  46. if (socket != null)
  47. socket.close();
  48. } catch (InterruptedException e) {
  49. e.printStackTrace();
  50. } catch (IOException e) {
  51. e.printStackTrace();
  52. }
  53. }
  54. }

九、客户端读消息线程,然后是客户端读消息线程,这里又有一个要注意的地方,我们收到消息的时候,是不是要告诉用户?如何告诉呢?接口监听貌似是一个很好的办法,神马?不知道接口监听?你会用Android的setOnClickListener不?这就是android封装好的点击事件监听,不懂的话,可以好好看看,理解一下,其实也不难:

[java]  view plain copy
  1. /**
  2. * 客户端读消息线程
  3. *
  4. * @author way
  5. *
  6. */
  7. public class ClientInputThread extends Thread {
  8. private Socket socket;
  9. private TranObject msg;
  10. private boolean isStart = true;
  11. private ObjectInputStream ois;
  12. private MessageListener messageListener;// 消息监听接口对象
  13. public ClientInputThread(Socket socket) {
  14. this.socket = socket;
  15. try {
  16. ois = new ObjectInputStream(socket.getInputStream());
  17. } catch (IOException e) {
  18. e.printStackTrace();
  19. }
  20. }
  21. /**
  22. * 提供给外部的消息监听方法
  23. *
  24. * @param messageListener
  25. *            消息监听接口对象
  26. */
  27. public void setMessageListener(MessageListener messageListener) {
  28. this.messageListener = messageListener;
  29. }
  30. public void setStart(boolean isStart) {
  31. this.isStart = isStart;
  32. }
  33. @Override
  34. public void run() {
  35. try {
  36. while (isStart) {
  37. msg = (TranObject) ois.readObject();
  38. // 每收到一条消息,就调用接口的方法,并传入该消息对象,外部在实现接口的方法时,就可以及时处理传入的消息对象了
  39. // 我不知道我有说明白没有?
  40. messageListener.Message(msg);
  41. }
  42. ois.close();
  43. if (socket != null)
  44. socket.close();
  45. } catch (ClassNotFoundException e) {
  46. e.printStackTrace();
  47. } catch (IOException e) {
  48. e.printStackTrace();
  49. }
  50. }
  51. /**
  52. * 消息监听接口
  53. *
  54. * @author way
  55. *
  56. */
  57. public interface MessageListener {
  58. public void Message(TranObject msg);
  59. }
  60. }

Java之Socket简单聊天实现(QQ续二)相关推荐

  1. java客户端服务器聊天程序流程图_基于java的socket简单聊天编程

    socket编程: 一:什么是socket:socket是BSD UNIX的通信机制,通常称为"套接字",其英文原意是"孔"或"插座".有些 ...

  2. java socket编程 聊天_基于java的socket简单聊天编程

    socket编程: 一:什么是socket:socket是BSD UNIX的通信机制,通常称为"套接字",其英文原意是"孔"或"插座".有些 ...

  3. java 网络编程简单聊天_网络编程之 TCP 实现简单聊天

    网络编程之 TCP 实现简单聊天 客户端 1.连接服务器 Socket 2.发送消息 package lesson02;import java.io.IOException;import java.i ...

  4. Java基于Socket实现聊天、群聊、敏感词汇过滤功能

    首先的话,这个代码主要是我很久以前写的,然后当时还有很多地方没有理解,现在再来看看这份代码,实在是觉得丑陋不堪,想改,但是是真的改都不好改了- 所以,写代码,规范真的很重要. 实现的功能: 用户私聊 ...

  5. 手机和电脑基于java的socket简单通信

    Java手机与电脑的Socket通信 了解手机与电脑的socket通信 下面给出代码 完整程序代码 效果 了解手机与电脑的socket通信 1.内网之间的通信: 内网就是两者处于同一个局域网之中,不用 ...

  6. java使用socket网络编程实现qq互聊(UPD简单版本)

    JAVA初学者.勿喷. eclipse稍微有点难用,关程序是在Console里面,要是没开出来并且死循环的话电脑cpu直接拉满了. 而且进程的关闭是输入指定的"再见"才关闭多线程, ...

  7. LINUX下UDP实现消息镜像通信,linux环境下基于udp socket简单聊天通信

    客户端代码:client.c /* * File: main.c * Author: guanyy * * Created on 20161202 * * 主要实现:客户端和服务端相互通信 */ #i ...

  8. Java的socket简单语法实例以及多线程

    1.服务期实现以及多线程加入 public class Sever {public static void main(String[] args) throws Exception{ServerSoc ...

  9. android socket 简易聊天室 java服务器,Android Socket通信实现简单聊天室

    socket通信是基于底层TCP/IP协议实现的.这种服务端不需要任何的配置文件和tomcat就可以完成服务端的发布,使用纯java代码实现通信.socket是对TCP/IP的封装调用,本身并不是一种 ...

最新文章

  1. python文件io是啥意思_Python文件IO(普通文件读写)
  2. SwiftTheme--iOS换肤解决方案
  3. 处理器中的内存管理单元
  4. [BX] 和 loop指令
  5. java media_unmount file_(20120801)android文件的读写SD卡总结
  6. NXP(I.MX6uLL) UART串口通信原理————这个未复习
  7. php怎样连接mysql_php怎么连接数据库
  8. axure中怎么把图片变圆_怎么将图片中的文字提取出来?收下这份识别教程
  9. mysql var目录很快_mysql数据库实现亿级数据快速清理的方法
  10. atitit.修复xp 操作系统--重装系统--保留原来文件不丢失
  11. java笔试+面试总结(大纲)
  12. 试用officescan 10.5
  13. [转]最世界最牛人博客,你可以学习到太多太多`~~
  14. J2Cache+Spring注入配置参数,无需读取固定路径下的j2cache.properties配置文件
  15. 机器人关节伺服电机PID串级控制
  16. 中国第一程序员求伯君,WPS之父,雷军也佩服的人
  17. Win32编程之基于MATLAB与VC交互的幻方阵(魔方阵)输出
  18. xlsxwriter去掉网格线_xlsxwriter图表网格间距
  19. Elementui el-select创建条目的多选下拉框 自定义校验 新增条目时字符长度限制
  20. 一个手机号可以注册绑定5个百度网盘,永久2T

热门文章

  1. TCHAR 宽字节的sprintf
  2. 【C语言精讲】——创建数组、使用数组(一维数组、二维数组)
  3. 调试小妙招之360随身WIFI设置两个热点
  4. Android 百度地图显示定位小蓝点
  5. Oracle数据库rownum用法详解
  6. Tensorflow.whl文件安装经验
  7. 华为黑科技,轻松提升电池续航的小技巧
  8. 人工智能基础之人工智能研究进展及领域
  9. eclipse git切换分支
  10. Ps大片教程:—失落之城