使用java Socket编写简单的联机井字棋游戏
Socket套接字简介
Socket套接字是在应用层和运输层之间的一个假象层(可以这莫理解),它向下包装了其他四层的操作,对程序员来说是透明的,也就是说,实际上C/S是在socket这一层上进行通讯的。
1.Socket建立连接
java中Socket把对象分为两种ServerSocket和Socket分别对应着服务端和客户端,我们不探究socket的具体原理有兴趣的可以看以下文章
链接: C++知识分享: Socket 编程详解,万字长文
接下来我们来建立简单的连接:
客户端代码:
//8080表明我们将服务器放在了8080端口
ServerSocket serverSocket=new ServerSocket(8080);
//这里阻塞的接收一个连接
Socket socket=serverSocket.accept();
服务端代码:
//向服务端发送连接请求返回一个socket对象
Socket socket = new Socket("localhost", 8080);
建立连接代码很简单,但是单纯建立一个连接又没有什么,我们建立连接就是为了交互,所以我们需要一点逻辑代码:最简单的输入输出
//输入流从另一端的输入读入
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
//输出流输出到另一端
PrintWriter printWriter = new PrintWriter(socket.getOutputStream(), true);
//读入使用bufferedReader.readline();
//输出则使用printWriter.println();
//这其中还有很多细节,譬如bufferedReader.readline();是阻塞函数等等,这是需要根据实现场景改变的不进行讨论
此时我们已经可以简单的使用Socket了如果你尝试了通讯,那你会发现只能输入一次,然后连接就断开了,这是因为如果socket连接不处于活跃状态,那么连接就会断开,所以我们需要让这个连接一直保持活跃,“心跳包”什么的不需要,我们需要添加一个while循环。
2.主要逻辑
//Server中的主要逻辑
while (true) {
//为线程添加锁
synchronized (lock) {
if(win.getWin()) {
printWriter.println("win");
printWriter.println("游戏结束");
printWriter.println(toString.ArrayToString(map));
//如果在此直接break,那么输出流就会关闭,连接断开,此时客户端就收不到信息
//所以在此接收一个bufferedReader
message=bufferedReader.readLine();
System.out.println(message);
break;
}
//如果已经赢了那么就退出
printWriter.println(toString.ArrayToString(map));
message = bufferedReader.readLine();
// System.out.println(message);
//只有输入合法位置时才能往下进行
while (!go(message, model)) {
printWriter.println("error");
message = bufferedReader.readLine();
}
//下棋之后进行判断
if (judge())
win.setWin(true);
printWriter.println(toString.ArrayToString(map));
}
//在释放锁后让线程睡眠100毫秒,确保下一次拿到锁的是另一个线程
Thread.sleep(100);
}
bufferedReader.close();
printWriter.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//服务端主要逻辑
while ((message=bufferedReader.readLine())!=null) {
// 从服务器接收消息
//如果收到的是错误,说明下的位置不对
//在此处继续输入
if(message.equals("error")) {
System.out.println("该位置已有棋子,请换位置");
String send = input.readLine();
printWriter.println(send);
}
//收到胜利信息后
else if(message.equals("win")) {
message=bufferedReader.readLine();
System.out.println(message);
message=bufferedReader.readLine();
client.map = toString.StringToArray(message);
client.picture();
printWriter.println("确认收到");
break;
}
//渲染画面
else {
System.out.flush();
client.map = toString.StringToArray(message);
client.picture();
String send = input.readLine();
printWriter.println(send);
message= bufferedReader.readLine();
client.map = toString.StringToArray(message);
client.picture();
}
}
接下来就是运行App
public static void main(String[] args) throws IOException {
Object lock=new Object();
ServerSocket serverSocket=new ServerSocket(8080);
Socket socket=serverSocket.accept();
// System.out.println("客户端已连接,地址:" + socket.getRemoteSocketAddress());
// Thread thread1=new Thread(new ServerThread(socket,lock),"1");
// Socket socket1=serverSocket.accept();
// System.out.println("客户端已连接,地址:" + socket1.getRemoteSocketAddress());
// Thread thread2=new Thread(new ServerThread(socket1,lock),"2");
//错误:这里的ServerThread不是同一个对象,所以里面的map是不共享的
//共享map
//将基本类型包装成对象进行共享
Win win=new Win();
ArrayList<Integer> map=new ArrayList<>();
map=new ArrayList<>();
for (int i=0;i<10;i++)
{
map.add(0);
}
System.out.println("客户端已连接,地址:" + socket.getRemoteSocketAddress());
Thread thread1=new Thread(new ServerThread(socket,map,win,lock),"1");
Socket socket1=serverSocket.accept();
System.out.println("客户端已连接,地址:" + socket1.getRemoteSocketAddress());
Thread thread2=new Thread(new ServerThread(socket1,map,win,lock),"2");
thread1.start();
thread2.start();
//
}
注释为编写过程中遇到的问题及解决办法
2.1 运行流程
当GameServer启动后会且只接受两个客户端的连接请求,在接收两个请求以后,会为两个请求开启不同的线程ServerThread,为其传入相同的obj来作为线程的锁和同一个map对象以保证运行的顺序如我们期待的那样
然后就是普通的传输数据和数据处理
每下一步棋客户端会向服务器端发送信息,服务端将他解析然后更新map中的值,因为map是共享对象,所以更新的结果会同步到两个线程
每次数据更新就会向客户端发送信息然后客户端重新渲染并输出,直到有一方胜利