프로그래밍/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;
	}