본문 바로가기

프로그래밍/C 프로그래밍

epoll을 이용한 서버 & java NIO를 이용한 클라이언트

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;
	}