안녕하세요 jsieun 입니다.
사용법
- http://jsieun73.tistory.com/140
새롭게 업데이트된 nio-서버 구조에 대해서 사진으로 쉽게 설명하자면,
아래 사진이라고 보시면 되겠습니다.
뼈대라고 부르는 JNetServer.java 안에 Bind, Accept, Read, Write, Disconnect, 등등..
서버에 관련된 것들이 들어있고, ServerStart.java 안에는 클라이언트로 부터 받은 데이터를 가지고
이벤트 처리하는 즉, RecvMessage 가 있습니다.
그리고 사용자 편의를 위해서 GUI 를 넣어서 1초마다 Connect , DisConect, RegMsg 의 값을
갱신해주는 JSFrame.class 이렇게 있습니다.
그럼 먼저 뼈대라고 부르는 소스부터 보도록 하겠습니다.
package JSieunPlatform;
/*
* 2017_07_09 2.0Version.
* Copyright 2017. J.sieun all rights reserved.
* 마음껏 사용하도 되나, 제이름은 적어주시길 바랍니다.
* */
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.StandardSocketOptions;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class JNetServer extends ServerStart implements Runnable {
//================
// 변수들.
//================
protected Selector Selector; //서버 셀렉터.
protected ServerSocketChannel ServerChannel = null; //서버 채널.
protected SocketChannel SocketChannel = null; //소켓 채널.
public static InetSocketAddress socketAddress; //IP주소.
private int port = 20000; //포트번호.
private boolean bstart = true; //서버스레드 진행or멈춤.
private final int THREAD_CNT = 5; //쓰레드 풀 갯수
private ExecutorService threadPool = null; //쓰레드풀
protected boolean aceptLimit = false; //Accept 제한.
protected ByteBuffer RecvBuf; //Recv 버퍼
protected SelectionKey RecvKey; //Recv Key값
public JNetServer(){
//포트설정
socketAddress = new InetSocketAddress(port);
threadPool = Executors.newFixedThreadPool(THREAD_CNT);
//받을 Buffer 크기 설정
this.RecvBuf = ByteBuffer.allocate(1000);
//서버 스레드시작
Thread thread = new Thread(this);
thread.start();
}
//IP주소와 PORT 번호 따오기.
public static String LocalIP() throws UnknownHostException {
InetAddress inetAddres = InetAddress.getLocalHost();
return "IP : " + inetAddres.getHostAddress() + " Port : " + socketAddress.getPort();
}
//스레드시작.
@Override
public void run() {
try {
Selector = java.nio.channels.Selector.open();
ServerChannel = ServerSocketChannel.open();
//Non-Blocking 실행
ServerChannel.configureBlocking(false);
//포트번호 지정.
ServerChannel.socket().bind(socketAddress);
//받는 버퍼사이즈 1000, 종료되면 포트해제(소켓설정)
ServerChannel.setOption(StandardSocketOptions.SO_RCVBUF, 128*1000);
ServerChannel.setOption(StandardSocketOptions.SO_REUSEADDR, true);
ServerChannel.register(Selector, SelectionKey.OP_ACCEPT);
} catch (Exception e) {
System.out.println("Server Run Error " + e.getMessage());
}
//Server Success
while (bstart) {
try {
//서버 채널이 준비되어 있는지 선택함.
Selector.select();
Iterator <SelectionKey> Selectkeys = Selector.selectedKeys().iterator();
//클라이언트가 송신올때 Key의 상태에 의해 Accept으로 등록하거나,Read해서 이벤트 처리하거나.
while (Selectkeys.hasNext()) {
SelectionKey key = Selectkeys.next();
Selectkeys.remove();
switch (key.interestOps()) {
case SelectionKey.OP_ACCEPT:
// Accept상태.
Accept(key);
break;
case SelectionKey.OP_READ:
// Read상태.
ReadPacket(key);
break;
case SelectionKey.OP_WRITE:
break;
default:
// 이상한 놈이 들어올땐 열결해제
disConnect(key);
break;
}
}
} catch (Exception e) {}
}
}
//들어온 클라이언트에 대해 소켓 생성
protected void Accept(SelectionKey key) {
//클라이언트 수제한 1000개이상 넘어가면 못받게
if(Selector.keys().size() > 1000) {aceptLimit = true;}
else if(Selector.keys().size() <=1000) {aceptLimit = false;}
if(!aceptLimit)
{
// 전달 받은 SelectionKey로 Accept 진행
ServerChannel = (ServerSocketChannel) key.channel();
SocketChannel channel = null;
try {
//채널에 Accept 하고,Non-Blocking, 읽기 가능한 상태로 등록.
channel = ServerChannel.accept();
channel.configureBlocking(false);
channel.register(Selector, SelectionKey.OP_READ);
JSFrame.ConectCnt();
} catch (Exception e) {
System.out.println("Accept Error " + e.getMessage());
}
}
}
//데이터를 읽는부분
protected void ReadPacket(SelectionKey key) {
//데이터의 키값을 채널로 변환.
SocketChannel = (SocketChannel) key.channel();
this.RecvBuf.clear();
try {
//RecvBuf 에 데이터를 복사후 쓰기상태로 등록.
SocketChannel.read(this.RecvBuf);
SocketChannel.register(Selector, SelectionKey.OP_WRITE);
//key값과 받은데이터를 가지고 이벤트 처리.
RegMessage(key,this.RecvBuf,this);
} catch (Exception ex) {
disConnect(key);
}
}
//보내기
protected void Send(JNetPacket packet, SelectionKey NetId) {
SocketChannel = (SocketChannel) NetId.channel();
try {
SocketChannel.write(packet.getBuf());
// 다시 읽을수 있게끔.
SocketChannel.register(Selector, SelectionKey.OP_READ);
} catch (Exception ex) {
//stateMsg("Send Error : " + ex.getMessage() + "\n");
}
}
//전체보내기
protected void SendAll(JNetPacket packet)
{
//로그인한 사람 한해서 전송
Iterator <String> itr = ServerStart.LoginMap.keySet().iterator();
while(itr.hasNext())
{
try{
//채널들 받기
SelectionKey NetId = ServerStart.LoginMap.get((String)itr.next());
SocketChannel = (SocketChannel)NetId.channel();
SocketChannel.write(packet.getBuf());
SocketChannel.register(Selector, SelectionKey.OP_READ);
}catch(Exception ex){ex.printStackTrace();}
}
}
//동기화특성땜에 기본값보내기
protected void RecvSend(SelectionKey NetId)
{
SocketChannel = (SocketChannel) NetId.channel();
JNetPacket RecvPacket = new JNetPacket();
RecvPacket.Type(JNetPacket.DEFAULT);
try{
SocketChannel.write(RecvPacket.getBuf());
SocketChannel.register(Selector, SelectionKey.OP_READ);
}catch(Exception ex){ex.printStackTrace();}
}
//그룹끼리보내기
protected void SendGroup(JNetPacket packet, String Sent)
{
//그룹으로 정해진 사람들로 하여금
Iterator <Long> itr = ServerStart.GroupMap.keySet().iterator();
while(itr.hasNext())
{
//링크리스트
Long key = itr.next();
LinkedList<String> list = ServerStart.GroupMap.get(key);
if(list.contains(Sent))
{
//수신자의 키값을 찾기
for(String GroupName : list)
{
//LoginMap을 통하여 클라이언트 NetId 받아내기.
SelectionKey NetId = ServerStart.LoginMap.get(GroupName);
SocketChannel = (SocketChannel) NetId.channel();
try{
//바이너리형식으로 보내고 다시 읽기 가능한 상태.
SocketChannel.write(packet.getBuf());
SocketChannel.register(Selector, SelectionKey.OP_READ);
}catch(Exception ex){ex.printStackTrace();}
}
}
}
}
//연결해제
protected void disConnect(SelectionKey NetId){
SocketChannel disChannel = (SocketChannel) NetId.channel();
Iterator<String> itr = ServerStart.LoginMap.keySet().iterator();
while (itr.hasNext()) {
// 연결해제할사용자 찾기
String disUser = itr.next();
SelectionKey disKey = ServerStart.LoginMap.get(disUser);
if (disKey.equals(NetId)) {
itr.remove();
// 사용자 찾음
ServerStart.LoginMap.remove(disUser);
// 그룹맵도 삭제
Iterator<Long> disItr = ServerStart.GroupMap.keySet().iterator();
while (disItr.hasNext()) {
// 링크리스트안에 있는 사용자 값을 삭제.
Long key = disItr.next();
LinkedList<String> list = ServerStart.GroupMap.get(key);
if (list.contains(disUser)) {
list.remove(disUser);
}
}
}
}
JSFrame.DisConectCnt();
try {
//채널종료.
disChannel.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//NetId 삭제.
NetId.attach(null);
NetId.cancel();
}
}
JNetServer.java 소스입니다.
소스해석은 이미 주석으로 깔끔하게 해서 그렇게 할필요는 없어보입니다.
(궁금한부분이 있으면, 댓글로 남겨주시면 됩니다^^, 자질구래한것도 응답해드립니다.)
=======Nio방식이란?=======
(지금부터는 제가 지금까지 서버와 클라이언트를 C/C++,C#,Java로 구현해보고,
공부하면서 얻은 지극히 주관적인 의견입니다. )
nio 즉, Non-Blocking 이라고 하는 것이 무엇인지 간단히 설명해보겠습니다.
블록킹상태란? 내가 데이터를 보내기전까지 얘내들은 아무것도 못하는 상태이고,
논블록킹 상태는 내가 데이터를 보내면서 다른일을 할수 있는 상태를 뜻합니다.
이거와 비슷한 예로 동기식 소켓구현과, 비동기식 소켓구현이 있습니다.
동기식이란, 클라이언트가 서버한테 패킷을 보내고 받기(무조건 받아야함)까지 클라는 대기상태로 변해서 그동안 아무것도 못합니다.
즉, 서버가 패킷을 보내는게 늦어지면 클라이언트도 계속 로딩중.... 이러한 화면이 나오게 됩니다.
예를들면,
은행 앱에서 로그인을 할경우, "로딩중..." 이러고 아무것도 못하게 되는 데 이걸 동기식이라고 생각하시면 될거 같습니다.
비동기식은 내가 서버한테 패킷을 보내고 "머 언젠간 패킷을 받겠지~" 이러면서 클라이언트는 할일 하게 됩니다.
하지만, 동기화를 잘처리해줘야합니다.(정말 중요합니다.)
예를 들면,
FPS게임을 하다보면 총을 쏴서 적군을 맞추게 되는데, 그때 적군을 맞추고 멈추는것이 아닌, 적군을 맞추고 아주 자유롭게 움직일수 있고, 다른 일들을 할수 있는데 이것을 비동기식이라고 생각하시면 될거 같습니다.
비동기식,동기식,블록킹,논블록킹을 정말 간단히 설명해보았는데요,
이제부터는 저 위에 있는 소스에서 "SocketChannel" 을가지고 클라이언트의 NetId를 가지고, 원하는 클라이언트에게 패킷을 보내는 것을 볼수가 있습니다. 여기서 NetId 가 어떤거냐면,
서버를 Power ON! 하고, 클라이언트는 접속하게 되는데, 그때 접속한 클라이언트들 마다 이름을 할당해줍니다.
그걸 보고 저는 NetId 라고 부르는 것입니다. (key값이라고 하는 경우도 있습니다.)
음..중단점을 걸어서 확인해보면
java.nio.channels.SocketChannel[connected local=/IP주소입니다~:20000 remote=/IP주소입니다~:14329]
이런식으로 숫자가 지정되어있습니다.
이렇게 목적지가 정해져있으니, 원하는 상대와 그룹화를 제대로 처리해줄수 있습니다.
아 그리고, TCP와 UDP 의 차이점 궁금해 하실텐데요
이것또한 간단히 말해드리겠습니다.
TCP는 안전빵 but, 느림(그래봤자 사람이 눈치채는 속도는 아니라고봄.인터넷환경이 70년대면 눈치챌수도?)
UDP는 굇수, but 노안전 (IP계의 베스같은놈)
정말 간단하죠?ㅋㅋㅋㅋ 우리나라 같은 경우에는 거진다 TCP를 사용한다고 보면되고, 외국같은 경우에는
정말 반응속도에 예민한 게임 같은 경우, UDP를 사용한다고들 합니다.(여담..)
이상 포스팅을 마치도록 하겠습니다. 감사합니다.