프로그래밍/C 프로그래밍
epoll을 이용한 서버 & java NIO를 이용한 클라이언트
오픈이지
2013. 6. 5. 16:38
epoll을 이용해 대용량 데이터 처리가 가능한 C로 작성된 서버
epoll을 이용하여 클라이언트의 요청을 받아서 처리하는 서버 sample.
서버는 클라이언트가 요청하는 데이터를 구조체에 담아서 처리하고, 처리된 결과를 클라이언트에게 전송한다.
[C로 작성된 서버 server.c]
#include#include #include #include #include #include #include #include #include #include #include #include #include #define EPOLL_SIZE 60 #define EPOLL_EVENT_SIZE 100 #define PORT 7777 #define BUFFSIZE 128 struct login_data { //수신된 데이터를 담기위한 구조체 char id[20]; char passwd[20]; char email[40]; char filler[48]; } data; int main (int argc, char **argv) { struct epoll_event *events; struct epoll_event ev; struct sockaddr_in addr, clientaddr; int clilen; int sfd, efd, cfd; int i; int max_got_events; int result; int readn; int sendflags = 0; int optval = 1; char buf_in[BUFFSIZE] = { '\0' }; struct login_data *p; if ((efd = epoll_create (EPOLL_EVENT_SIZE)) < 0) { // 이벤트 처리를 위한 epoll을 생성한다. perror ("epoll_create (1) error"); return -1; } events = (struct epoll_event *) malloc (sizeof (*events) * EPOLL_SIZE); if (NULL == events) { perror ("epoll_create (0) error"); return -1; } clilen = sizeof (clientaddr); sfd = socket (AF_INET, SOCK_STREAM, 0); // 서버 소켓을 생성한다. if (sfd == -1) { perror ("socket error :"); close (sfd); return -1; } setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof optval); // 서버 소켓의 포트번호가 재사용가능하도록 소켓 옵션을 조정한다. addr.sin_family = AF_INET; addr.sin_port = htons (PORT); addr.sin_addr.s_addr = htonl (INADDR_ANY); if (bind (sfd, (struct sockaddr *) &addr, sizeof (addr)) == -1) { //port번호를 소켓에 바인딩한다. perror("bind error"); close (sfd); return -2; } if ( listen (sfd, 5) < 0) { perror ("init_acceptsock error"); return 1; } printf("서비스 시작 [%d]\n",PORT); ev.events = EPOLLIN; ev.data.fd = sfd; if ( epoll_ctl (efd, EPOLL_CTL_ADD, sfd, &ev) < 0) { //서버 소켓을 epoll에 등록한다. 접속요청이 들어오면 읽기 이벤트가 발생한다. perror ("epoll_ctl error"); return 1; } while (1) { max_got_events = epoll_wait (efd, events, EPOLL_SIZE, -1); //이벤트가 발생하기를 대기한다. for (i = 0; i < max_got_events; i++) { if (events[i].data.fd == sfd) { //서버소켓에서 이벤트가 발생한 경우 printf("클라이언트 연결 수신\n"); cfd = accept (sfd, (struct sockaddr *) &clientaddr, &clilen); // 클라이언트의 연결요청을 처리한다. printf("연결됨: %s\n",inet_ntoa(clientaddr.sin_addr)); if (cfd < 0) { perror ("Accept error"); return -1; } ev.events = EPOLLIN; ev.data.fd = cfd; epoll_ctl (efd, EPOLL_CTL_ADD, cfd, &ev); // 새로 연결된 클라이언트와의 통신소켓에 대해, 읽기요청에 대한 이벤트처리가 되도록 설정하고 epoll에 등록한다. } else { cfd = events[i].data.fd; // 클라이언트로 부터 수신된 데이터가 있는 경우 memset (buf_in, 0x00, BUFFSIZE); readn = read (cfd, buf_in, BUFFSIZE); // 수신버퍼에 데이터를 읽어들인다. printf("읽은 바이트수: %d\n",readn); if (readn <= 0) { // 클라이언트의 연결이 종료되었으면, epoll에서 해당 소켓 정보를 제거하고 epoll_ctl (efd, EPOLL_CTL_DEL, cfd, &ev); close (cfd); // 클라이언트 소켓을 닫는다. printf ("클라이언트 연결종료[%d] ", cfd); } else { // 수신된 데이터가 있는 경우 int i=0; struct login_data * p=(struct login_data *)buf_in; // 수신버퍼를 처리하고자 하는 데이터 구조체로 형변환한다. printf("수신데이터: "); printf("%s, %s, %s\n",p->id,p->passwd,p->email); // 수신된 데이터를 콘솔에 출력한다. memset(buf_in,0x00,128); strncpy(p->id,"응답1",20); // 응답메시지를 송신하기 위한 버퍼에 저장한다. strncpy(p->passwd,"응답2",20); strncpy(p->email,"응답3",20); send (cfd, buf_in,BUFFSIZE , sendflags); // 응답메시지를 클라이언트에 전송한다. printf("송신데이터: "); printf("%s, %s, %s\n",p->id,p->passwd,p->email); } } } } return 1; }
java NIO를 이용한 자바 클라이언트
SocketChannel과 selector를 이용하여 서버와 통신할 수 있는 통신채널을 생성한다.
ByteBuffer에 전송할 데이터를 담아서 채널을 통해서 전송한다. 전송되는 데이터는 UTF-8로 인코딩 해서 전송한다. 수신되는 데이터도 ByteBuffer로 수신하며 수신된 데이터는 모델컴포넌트에 담아서 사용한다.
[SWING GUI를 가진 Main.java]
package kr.co.openeg.view; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import javax.swing.BoxLayout; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JMenu; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JTextField; import kr.co.openeg.controller.LoginComm; import kr.co.openeg.model.LoginData; public class LoginMain extends JFrame implements ActionListener, KeyListener { JLabel label1,label2,label3; JTextField text1,text2,text3; JButton button1, button2; JPanel panel1, panel2, panel3,panel4; JMenu menu2; JMenuItem mitem1,mitem2; LoginComm comm; public LoginMain() { comm= new LoginComm(); panel1=new JPanel(); panel2=new JPanel(); panel3=new JPanel(); panel4=new JPanel(); label1=new JLabel(" 아 이 디"); label2=new JLabel("패스워드"); label3=new JLabel(" 이 메 일"); text1=new JTextField(10); text2=new JTextField(10); text3=new JTextField(10); button1=new JButton("로그인"); button2=new JButton("취소"); button1.addActionListener(this); button1.addKeyListener(this); panel1.add(label1); panel1.add(text1); panel2.add(label2); panel2.add(text2); panel3.add(label3); panel3.add(text3); panel4.add(button1); panel4.add(button2); this.getContentPane().setLayout(new BoxLayout(this.getContentPane(), BoxLayout.Y_AXIS)); this.getContentPane().add(panel1); this.getContentPane().add(panel2); this.getContentPane().add(panel3); this.getContentPane().add(panel4); this.setSize(300, 200); this.setVisible(true); } @Override public void actionPerformed(ActionEvent e) { // 버튼이 클릭되었을때 서버에 요청을 전송한다. // TODO Auto-generated method stub comm.send(new LoginData(text1.getText().toString(), text2.getText().toString(), text3.getText().toString())); LoginData data=comm.receive(); // 서버로 부터 데이터를 수신한다. text1.setText(data.getId()); // 수신된 데이터를 화면에 표시한다. text2.setText(data.getPasswd()); text3.setText(data.getEmail()); } @Override public void keyPressed(KeyEvent e) { // TODO Auto-generated method stub } @Override public void keyReleased(KeyEvent e) { // TODO Auto-generated method stub } @Override public void keyTyped(KeyEvent e) { // 전송버튼이 키로 눌러졌을때도 서버에 데이터를 전송한다. // TODO Auto-generated method stub comm.send(new LoginData(text1.getText().toString(), text2.getText().toString(), text3.getText().toString())); LoginData data=comm.receive(); text1.setText(data.getId()); text2.setText(data.getPasswd()); text3.setText(data.getEmail()); System.out.println("서버로부터 데이터 수신"); } /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub new LoginMain(); } }
[채널을 이용하여 통신을 담당하는 LoginComm.java]
package kr.co.openeg.controller; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.nio.charset.Charset; import java.util.Iterator; import kr.co.openeg.model.LoginData; public class LoginComm { private SocketChannel channel; private Selector selector; Charset utf8; public LoginComm() { try { utf8 = Charset.forName( "UTF-8" ); // 한글처리를 위한 Charset객체를 생성한다. selector=Selector.open(); // 멀티플렉싱을 위한 selector 객체를 생성한다. channel=SocketChannel.open(new InetSocketAddress("192.168.149.128",7777)); // 통신을 위한 소켓 채널을 생성한다. channel.configureBlocking(false); // Non Blocking 모드로 통신하도록 설정한다. channel.register(selector, SelectionKey.OP_READ); // 읽기 이벤트가 발생하면 채널로 부터 데이터를 읽어 오기위해 selector를 채널에 등록한다. }catch(Exception e) { e.printStackTrace(); } } public void send(LoginData data) { ByteBuffer buffer=ByteBuffer.allocate(128); // 데이터 전송을 위해 128바이트 짜리 ByteBuffer 객체를 생성한다. buffer.position(0); // 0+1번째위치에서 부터 화면에 입력된 ID값을 저장한다. buffer.put(data.getId().getBytes()); buffer.position(20); // 20+1번째위치에서 부터 화면에 입력된 PASSWD값을 저장한다. buffer.put(data.getPasswd().getBytes()); buffer.position(40); // 40+1번째위치에서 부터 화면에 입력된 PASSWD값을 저장한다. buffer.put(data.getEmail().getBytes()); buffer.flip(); // buffer를 읽기->쓰기로 변환 try { channel.write(buffer); // 통신채널을 통해 buffer의 데이터를 전송한다. System.out.println(toString(buffer,0,20)); System.out.println(toString(buffer,20,40)); System.out.println(toString(buffer,40,80)); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } buffer.clear(); buffer=null; } private static String toString(ByteBuffer buffer, int pos, int limit) throws UnsupportedEncodingException { byte[] bytes = new byte[limit-pos]; // ByteBuffer의 지정된 위치의 내용을 읽어서 저장하기 위한 버퍼 buffer.position(pos).limit(limit); // pos+1번째에서 부터 limit번째 까지의 데이터를 읽기위한 위치 설정 buffer.get(bytes); // limit-pos 길이 만큼의 바이트를 읽어낸다. return new String(bytes, "UTF-8"); // 바이트 배열을 UTF-8로 인코딩하여 문자열을 생성한 뒤 반환한다. } private static ByteBuffer toByteBuffer(String value) throws UnsupportedEncodingException { return ByteBuffer.wrap(value.getBytes("UTF-8")); // 바이트배열을 UTF-8로 인코딩하여 ByteBuffer 객체로 반환한다. } public LoginData receive(){ ByteBuffer buffer=null; LoginData data=null; try { selector.select(); buffer=ByteBuffer.allocate(128); Iterator iter=selector.selectedKeys().iterator(); // 읽기 이벤트가 발생된 이벤트들이 반환된다. while(iter.hasNext()){ SelectionKey key=(SelectionKey)iter.next(); // 발생된 이벤트를 하나씩 읽어낸다. SocketChannel sc=(SocketChannel)key.channel(); // 이벤트가 발생한 채널 객체를 얻어온다. sc.read(buffer); // 해당 채널로 부터 ByteBuffer에 데이터를 읽어 들인다. data=new LoginData(); buffer.flip(); data.setId(toString(buffer,0,20).trim()); data.setPasswd(toString(buffer,20,40).trim()); data.setEmail(toString(buffer,40,80).trim()); } }catch(Exception e){ e.printStackTrace(); } System.out.println("수신데이터: "+data.getId()+","+data.getPasswd()+","+data.getEmail()); return data; } }
[ValueObject 객체를 구현하는 LoginData.java]
package kr.co.openeg.model; public class LoginData { private String id; private String passwd; private String email; public LoginData() {} public LoginData(String id, String passwd, String email) { super(); this.id = id; this.passwd = passwd; this.email = email; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getPasswd() { return passwd; } public void setPasswd(String passwd) { this.passwd = passwd; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; }