안녕하세요 nio 를 이용한 자바 소켓 서버부분에 대해서 포스팅을 해보도록 하겠습니다.
전 아직 "학생"이기 때문에 10년? 15년? 도 더 된 기능을 가지고 하는거라 너무 질타는 하지 말아주시길 바랍니다.ㅜㅜ 나중엔 AsynchronousServerSocketChannel (비동기) 에 대해서도 공부할 계획입니다!! :D
우선 서버 부분 캡처 사진입니다.
Accept IP 부분은 클라이언트가 접속한 Channel 의 정보이고,
RecvIP 부분은 어떠한 클라이언트가 데이터를 주고받는지에 대해서 알려주는 부분이 되겠습니다.
그럼 소스를 보도록 하겠습니다.
소스를 보기전에 이소스는 제가 열심히 공부하면서 손수 작성하게된 소스이므로, 마음껏 사용하셔도 되지만,
이름은 변경하지 말아주시길 바랍니다.
(또한, 이러한 기능이 있었으면 좋겠다 또는, 안되는 부분은 바로바로 댓글로 피드백 해주시길 바랍니다 :D)
소스보기 접기
package JSieunPlatform;
/* * 2017_04_11 * Copyright 2017. J.sieun all rights reserved. * 마음껏 사용하도 되나, 제이름은 적어주시길 바랍니다. * */ import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.AdjustmentEvent; import java.awt.event.AdjustmentListener; import java.io.IOException; import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketAddress; import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.HashMap; import java.util.Iterator; import java.util.Vector;
import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollBar; import javax.swing.JScrollPane; import javax.swing.JTextPane; import javax.swing.text.SimpleAttributeSet; import javax.swing.text.StyledDocument;
public class JServer extends JFrame implements Runnable, ActionListener, AdjustmentListener {
// 변수들 private JButton startButton, exitButton; private JScrollBar bar; private JTextPane statePane; private SimpleAttributeSet attriState; private StyledDocument docState; private JLabel titleLabel, stateLabel;
// 해쉬맵 NetId 저장 private HashMap<SocketChannel, String> _ConnectMap; private HashMap<String, SelectionKey> _LoginMap; private Vector<String> _LoginVector; private ByteBuffer _Buf; // 소켓 채널 private Selector _Selector; private ServerSocketChannel _ServerChannel = null; private SocketChannel _SocketChannel = null; private InetSocketAddress socketAddress; private String _address = "127.0.0.1"; private int _port = 7273; private boolean _bstart = true; // Send&RecvPacket private JPacket JNetPacket = null;
public JServer() { super("JSieun73 Server"); socketAddress = new InetSocketAddress("localhost", _port); _ConnectMap = new HashMap<SocketChannel, String>(); _LoginMap = new HashMap<String, SelectionKey>(); _LoginVector = new Vector<String>(2, 1); JNetPacket = new JPacket(); this._Buf = ByteBuffer.allocate(128); this._Buf.clear(); }
public void initialize() { JPanel north_Pn = new JPanel(new FlowLayout(FlowLayout.LEFT, 15, 15));
stateLabel = new JLabel("\tClick Start Button...\t", JLabel.CENTER); stateLabel.setOpaque(false); north_Pn.add(stateLabel); JPanel center_Pn = new JPanel(new BorderLayout()); titleLabel = new JLabel("\tSERVER MESSAGE\t", JLabel.CENTER);
statePane = new JTextPane(); statePane.setBorder(BorderFactory.createRaisedBevelBorder()); statePane.setEditable(false);
JScrollPane scroll = new JScrollPane(statePane, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); scroll.setOpaque(false);
bar = scroll.getVerticalScrollBar(); docState = statePane.getStyledDocument(); center_Pn.add(titleLabel, "North"); center_Pn.add(scroll, "Center");
JPanel south_Pn = new JPanel(); startButton = new JButton(" START "); startButton.addActionListener(this);
exitButton = new JButton(" EXIT "); exitButton.setEnabled(false); exitButton.addActionListener(this);
south_Pn.add(startButton); south_Pn.add(exitButton);
getContentPane().add(north_Pn, "North"); getContentPane().add(center_Pn, "Center"); getContentPane().add(south_Pn, "South");
setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); Toolkit kit = getToolkit(); Dimension dmen = kit.getScreenSize(); setLocation((int) ((dmen.width - getSize().width) / 8), (int) ((dmen.height - getSize().height) / 8)); setSize(300, 300); setResizable(false); setVisible(true);
}
@Override public void adjustmentValueChanged(AdjustmentEvent e) { // TODO Auto-generated method stub bar.setValue(bar.getMaximum()); bar.removeAdjustmentListener(this); }
@Override public void actionPerformed(ActionEvent e) { // TODO Auto-generated method stub Object o = e.getSource(); if (o.equals(startButton)) { startButton.setEnabled(false); exitButton.setEnabled(true); Thread thread = new Thread(this); thread.start(); stateLabel.setText("SERVER ON"); } if (o.equals(exitButton)) { BeforeExit(); _bstart = false; System.exit(0); } }
@Override public void run() { try { _Selector = Selector.open(); _ServerChannel = ServerSocketChannel.open(); _ServerChannel.configureBlocking(false); _ServerChannel.socket().bind(socketAddress); _ServerChannel.register(_Selector, SelectionKey.OP_ACCEPT); stateMsg("IP 주소 : " + socketAddress.getAddress() + "\n"); } catch (Exception e) { System.out.println("Server Run Error " + e.getMessage()); } stateMsg("Server Start!!\n"); while (_bstart) { try { _Selector.select(); Iterator<?> keys = _Selector.selectedKeys().iterator(); while (keys.hasNext()) { SelectionKey key = (SelectionKey) keys.next(); keys.remove(); if (!key.isValid()) { // 사용가능한 상태가 아니면 그냥 넘어감 continue; } if (key.isAcceptable()) { // Accept가 가능한 상태라면 Accept(key); } else if (key.isReadable()) { // 데이터를 읽을수 있는 상태라면 stateMsg("Recv IP & Port : " + _ConnectMap.get(key.channel()) + "\n"); ReadPacket(key); } } } catch (Exception e) { // System.out.println("Server While Error " + e.getMessage()); } } }
private void Accept(SelectionKey key) { // 전달 받은 SelectionKey로 Accept 진행 _ServerChannel = (ServerSocketChannel) key.channel(); SocketChannel channel = null; try { channel = _ServerChannel.accept(); channel.configureBlocking(false); Socket socket = channel.socket(); SocketAddress remoteAddr = socket.getRemoteSocketAddress(); _ConnectMap.put(channel, remoteAddr.toString()); channel.register(_Selector, SelectionKey.OP_READ); stateMsg("Accept IP & Port : " + remoteAddr + "\n"); } catch (Exception e) { System.out.println("Accept Error " + e.getMessage()); } }
private void ReadPacket(SelectionKey key) { _SocketChannel = (SocketChannel) key.channel(); this._Buf.clear(); try { _SocketChannel.read(_Buf); } catch (Exception ex) { stateMsg("Read Error : " + ex.getMessage() + "\n"); } try { _SocketChannel.register(_Selector, SelectionKey.OP_WRITE); } catch (ClosedChannelException e) { // TODO Auto-generated catch block e.printStackTrace(); } RecvEvt(key); }
private void RecvEvt(SelectionKey key) { if (this._Buf.limit() == 0) { stateMsg("데이터가 올바르게 전송이 되지 않았습니다. \n다시 보내주시길 바랍니다.\n"); // 다시 재전송 하는 이벤트 추가 } this._Buf.flip(); switch (this._Buf.getShort()) { case JPacket.LOGIN: String _LoginName = JNetPacket.RecvOut(this._Buf); _LoginVector.addElement(_LoginName); _LoginMap.put(_LoginName, key); LoginState(); SendAll(JNetPacket); break; case JPacket.DISCONNECT: String _DisconName = JNetPacket.RecvOut(this._Buf); stateMsg(_DisconName + "님이 로그아웃하였습니다. \n"); Disconnet(key,_DisconName); break; case JPacket.CHAT: String ChtName = JNetPacket.RecvOut(this._Buf); String ChtMsg = JNetPacket.RecvOut(this._Buf); JNetPacket.Clear(); JNetPacket.Type(JPacket.CHAT); JNetPacket.Add(ChtName); JNetPacket.Add(ChtMsg); SendAll(JNetPacket); break; case JPacket.FIGHT: break; case JPacket.WHISPER: String WhiRecvName = JNetPacket.RecvOut(this._Buf); String WhiName = JNetPacket.RecvOut(this._Buf); String WhiMsg = JNetPacket.RecvOut(this._Buf); SelectionKey WhiKey = _LoginMap.get(WhiName); JNetPacket.Clear(); JNetPacket.Type(JPacket.WHISPER); JNetPacket.Add(WhiRecvName); JNetPacket.Add(WhiMsg); Send(JNetPacket, WhiKey); RecvSend(key); break; case JPacket.TESTPACKET: String TestPacket = JNetPacket.RecvOut(this._Buf); String TestPacket1 = JNetPacket.RecvOut(this._Buf); JNetPacket.Clear(); JNetPacket.Type(JPacket.TESTPACKET); JNetPacket.Add(TestPacket); JNetPacket.Add(TestPacket1); Send(JNetPacket, key); break; default: break; } }
// 상태메시지 public void stateMsg(String msg) { try { attriState = new SimpleAttributeSet(); statePane.setCaretPosition(docState.getEndPosition().getOffset() - 1); docState.insertString(statePane.getCaretPosition(), msg, attriState); bar.addAdjustmentListener(this); } catch (Exception e) { String _errorMsg = "*상태메시지 에러발생!!*"; JOptionPane.showMessageDialog(null, _errorMsg, "JServer", JOptionPane.INFORMATION_MESSAGE); } }
public void Send(JPacket packet, SelectionKey key) { _SocketChannel = (SocketChannel) key.channel(); try { _SocketChannel.write(packet.getBuf()); // 다시 읽을수 있게끔. _SocketChannel.register(key.selector(), SelectionKey.OP_READ); } catch (Exception ex) { stateMsg("Send Error : " + ex.getMessage() + "\n"); } }
public void SendAll(JPacket packet) { try { // 셀렉터를 깨워주고 _Selector.select(); } catch (Exception ex) { stateMsg("SendAll Error : " + ex.getMessage() + "\n"); } // 접속된클라 key값과 서버 key값까지포함것을 iterator 처리 Iterator<SelectionKey> _Iterator = _Selector.keys().iterator(); while (_Iterator.hasNext()) { SelectionKey _SelectionKey = _Iterator.next(); // 서버 key값은 Accept니까 서버는 제외함. if (!_SelectionKey.isAcceptable()) { _SocketChannel = (SocketChannel) _SelectionKey.channel(); try { _SocketChannel.write(packet.getBuf()); _SocketChannel.register(_SelectionKey.selector(), SelectionKey.OP_READ); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } else { } } _Iterator.remove(); } public void RecvSend(SelectionKey key) { _SocketChannel = (SocketChannel) key.channel(); JPacket RecvPacket = new JPacket(); RecvPacket.Type(JPacket.DEFAULT); try { _SocketChannel.write(RecvPacket.getBuf()); _SocketChannel.register(key.selector(), SelectionKey.OP_READ); } catch (Exception ex) { stateMsg("RecvSend Error : " + ex.getMessage() + "\n"); } }
// 서버 나가기 public boolean BeforeExit() { JLabel content = new JLabel("게임서버를 종료시키겠습니까?!"); int confirm = JOptionPane.showConfirmDialog(null, content, "종료확인", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); if (confirm == 1) return false; return true; }
// 접속한 로그인 정보 public void LoginState() { JNetPacket.Clear(); JNetPacket.Type(JPacket.LOGININFOR); JNetPacket.Add(_LoginVector.size()); for (int i = 0; i < _LoginVector.size(); i++) { JNetPacket.Add(_LoginVector.elementAt(i)); } }
// 클라 연결해제 public void Disconnet(SelectionKey key, String Name) { this._ConnectMap.remove(key); this._LoginMap.remove(Name); key.cancel(); JNetPacket.Clear(); JNetPacket.Type(JPacket.DISCONNECT); JNetPacket.Add(Name); SendAll(JNetPacket); } public static void main(String args[]) { JServer _server = new JServer(); _server.initialize(); }
}
접기
제가 만든 자바 서버의 기능으로는
1. 원하는 상대에게 보내기
2. 자기자신에게 보내기
3. 접속한 모든 클라이언트에게 보내기
4. 연결 해제
이러한 기능들이 있습니다.
그럼 소스 해석을 해보도록 하겠습니다.
1.run() 메소드
이부분을 한마디로 정의하면 서버를 실행시키고 그 서버에 대한 SelectionKey 의 값은 "접속가능한 상태" 로 설정한다. 라고 알고계시면 되겠습니다.
서버에서 가장 중요하다고 생각되는 부분이 이부분인데요
이부분에서 "keys" 라는 변수가 핵심인데요,
클라이언트가 서버한테 접속할려고 할때에는 key 값이 Accept 가능상태입니다. 좀더 이해하기 쉽게
중단점을 걸어서 보여드리도록 하겠습니다.
이렇게 보시게 되면 처음에 클라이언트는 Accept 상태가 true 이고 Read 상태는 false 입니다. 왜냐하면
Accept 를 넘어간후, 그다음에 데이터를 받을수있는 Read 상태로 넘어가야 하기 때문에 처음에 클라이언트가 서버에 접속했을때에는 Accept 가 true로 되어야 합니다.
만약에 데이터를 보냈을 경우에는
위 사진과 같이 Accept 는 false 이고, Read 는 true 상태가 되어서 데이터를 읽기 시작합니다.
2. Accept 메소드
이메소드 같은 경우에는 말그대로 클라이언트가 서버로 접속된것들을 처리해주는 메소드라고 보시면 되겠습니다.
여기서 유심히 보아야 할부분은
channel.configureBlocking(false) 인데요
이부분은 자바 nio 에만 있는 기능입니다. (근데 nio 가 non-blocking 이 아닌거 같습니다. 즉, 비동기가 아니란 셈이죠. 지극히 제생각엔 말이죠..)
추가적으로 여기서 _Selector 에서 접속한 클라이언트를 등록을 해줍니다.
3.ReadPacket 메소드
이 메소드는 클라이언트로 부터 받은 데이터를 읽어내는 부분이 되겠습니다.
딱히....그렇게 중요해보이는 소스는 없어서 다음으로 넘어갑니다.
4. RecvEvt 메소드
이부분은 데이터를 받은 부분을 처리해주는 메소드가 되겠습니다.
여기서 중요한 부분은 this._Buf.flip() 메서드인데요.
ByteBuffer 를 사용하실때에는
1 . flip()
2. clear()
3. rewind()
이 3가지가 가장 중요하다고 생각이 듭니다.
차례로 한마디로 설명하면 flip 은 position 값을 0 으로 이동
rewind 은 데이터를 재사용할때 사용합니다. (거의 초기화라고 보시면 됩니다.)
clear() 모든 값들을 깔끔히 정리합니다.
이렇게 해서 데이터를 받으면 그의 맞게 처리를 합니다.
저같은 경우에는 패킷(서버&클라)에서 데이터를 보낼때
_Type
(2byte)
dataSize
(4byte)
data
(...)
이런식으로 구성했습니다. 처음에 어떠한 타입인지 인식후
데이터사이즈 + 데이터 이런식으로 구성되어있습니다.
(처음에는 앞에 전체 사이즈를 주면서 했는데 자바는 굳이 그렇게 할 필요가 없어서 맨앞에는 그냥 타입부터 설정하도록 하였습니다.)
5.Send 메소드
이 부분은 클라한테 보내는 메소드중 하나입니다. 말그대로 key 값을 가진 클라이언트한테 보내는 역활입니다.
이걸로 원하는 상대나 자기자신한테 보낼수 있습니다.
6.SendAll 메소드
이 메소드는 말그대로 접속된 클라이언트한테 전체적으로 데이터를 전송 역활을 합니다.
중간중간에 주석처리 되어있어서 굳이 설명할필요는 없을거 같습니다.
한마디로 정리하자면 _Selector 에 저장되어 있는 서버개설자와 클라이언트들을 Iterator 통하여 나열한후,
서버개설자인 (Accept 값이 true 인 key값)를 제외하고 나머지에게 데이터를 전송합니다.
7.RecvSend 메소드
이 메소드는 제가 귓속말기능 (원하는 상대에게 데이터를 보내는 기능) 을 구현하다가 만들게 되었는데요,
그냥 Send 랑 별반 다를게 없어보이지만, 또한 굳이 이걸 안만들어도 될듯싶었지만, 구분을 지어야 한다는 생각에
만들었습니다.
이유는 Server 에 A,B,C 클라이언트가 접속되어 있습니다.
그후, A클라이언트가 B클라이언트한테 데이터를 보내면 데이터는 그대로 전송이 되지만, A클라이언트가 먹통이 되는 현상이 걸리더군요 ㅡㅡ;
그래서 저는 A클라이언트가 데이터를 보냈는데 그이후에 데이터를 안받아서 그런거 같다고 중단점과 실험을 통해서 알게 되었습니다. 그래서 원하는 상대에게 데이터를 보낼때, 즉, A가 B한테 데이터를 보낼때 서버는 A->B 에게 데이터를 보내고 A->A 한테도 아무데이터를 보내는 것이 되겠습니다.
저도 이게 왜그런지는 잘모르겠지만, 아무쪼록 이러한 현상이 나와서 저런식으로 구현해서 해결을 하였습니다.
차차 공부하다보면 알게 되겠지만, 알다가도 모르는 문제인거 같습니다 _-_;;;
이후로는 그렇게 중요한 메소드라기보단, 부수적으로 필요한 기능 메소드라서 생략하도록 하겠습니다.
##제가 설명이 좀 많이 부족하지만, 궁금하시는 부분에서 댓글을 남겨주시면 친절히 답변해드리도록 하겠습니다.
조만간 클라이언트와 패킷 소스에 대해서도 포스팅하겠습니다.(시험기간 끝나고....어휴)
이상 포스팅을 마치도록 하겠습니다.
감사합니다.