개발 일기

소켓과 친해지기 본문

카테고리 없음

소켓과 친해지기

flow123 2023. 6. 18. 15:53

 

현재 팀에서 다루는 서버의 절반은 TCP 통신 기반이다.

자주 쓰이지만 익숙치 않은 개념이어서, TCP 통신과 관련해서 기초 네트워크 개념을 정리해보았다. 실무에서 자주 쓰이는 패킷도 들여다 보고자 한다. 

IO와 NIO 의 차이

출처: https://www.slideshare.net/kslisenko/networking-in-java-with-nio-and-netty-76583794

 

자바 Socket 은 blocking IO 로 동작한다. 

 

IO는 스트림(Stream)이라는 단방향 통로를 생성해서 외부 데이터와 통신, 연결 클라이언트 수가 적고 대용량, 순차처리에 적합하다. 

NIO는 채널(Channel)이라는 양방향 통로를 생성해서 외부 데이터와 통신, 연결 클라이언트 수가 많고 소규모 빠른 대응에 적합하다. 

IO NIO
스트림방식 Non-buffer 버퍼방식
동기방식 동기/비동기 모두 지원
블로킹 방식 블록킹/논블록킹 모두 지원

 

순수 Java NIO로 TCP Echo 서버 만들어보기

 


Selector : 여러개의 채널에서 발생하는 이벤트(연결이 생성됨, 데이터가 도착함 등) 를 모니터링할 수 있는 객체

ServerSocketChannel: 들어오는 TCP 연결을 수신할 수 있고, 멀티 쓰레드에 안전한 채널

SocketChannel: 위의 채널에서 연결을 가져와서, accept() 메서드로 새로운 커넥션을 위한 socket channel 을 반환

전체 코드 

EchoServer

package org.example;

import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
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.Set;

public class EchoServer {
    public static void main(String[] args) throws IOException {
        //open(): create a Selector object / channel
        Selector selector = Selector.open();
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //bind port, set selectable channel to non-blocking mode, register channel to the selector
        serverSocketChannel.bind(new InetSocketAddress("localhost", 8080));
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        System.out.println("server is ready");

        while (true) {
            selector.select();
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iter = selectionKeys.iterator();
            while (iter.hasNext()) {
                SelectionKey key = iter.next();
                if (key.isAcceptable()) {
                    register(selector, serverSocketChannel);
                }

                if (key.isReadable()) {
                    answerWithEcho(buffer, key);
                }
                iter.remove();
            }

        }
    }

    public static void register(Selector selector, ServerSocketChannel serverSocketChannel) throws IOException {
        SocketChannel client = serverSocketChannel.accept();
        client.configureBlocking(false);
        client.register(selector, SelectionKey.OP_READ);
    }

    private static void answerWithEcho(ByteBuffer buffer, SelectionKey key) throws IOException {
        //SocketChannel extends SelectableChannel
        SocketChannel client = (SocketChannel) key.channel();
        int point = client.read(buffer);
        //end of the input stream. no more bytes available to be read || may indicate error
        if (point == -1) {
            System.out.println("Not accepting client messages anymore");
            client.close();
        } else {
        	//읽기/쓰기 작업의 범위를 설정 (시작-끝) 
            //socketChannel(client)에 write(buffer) 전, 
            //클라이언트에게 데이터를 전송하기 전에 write mode 로 전환하는 역할
            //read(buffer) 후에 호출. → position을 0으로 세팅해서 버퍼의 시작부터 읽도록 함.
            buffer.flip();
            client.write(buffer);
            buffer.clear();
        }
    }

    public static Process start() throws IOException, InterruptedException {
        String javaHome = System.getProperty("java.home");
        String javaBin = javaHome + File.separator + "bin" + File.separator + "java";
        String classPath = System.getProperty("java.class.path");
        String className = EchoServer.class.getCanonicalName();
        ProcessBuilder builder = new ProcessBuilder(javaBin, "-cp", classPath, className);
        return builder.start();
    }
}

EchoClient 

package org.example;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class EchoClient {
    private static SocketChannel client;
    private static ByteBuffer buffer;
    private static EchoClient instance;

    public static EchoClient start() {
        if(instance == null)
            instance = new EchoClient();
        return instance;
    }

    public static void stop() throws IOException {
        client.close();
        buffer = null;
    }

    private EchoClient() {
        try {
            client = SocketChannel.open(new InetSocketAddress("localhost", 8080));
            buffer = ByteBuffer.allocate(256);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public String sendMessage(String msg) {
        buffer = ByteBuffer.wrap(msg.getBytes());
        String response = null;
        try {
            client.write(buffer);
            buffer.clear();
            client.read(buffer);
            response = new String(buffer.array()).trim();
            System.out.println("response=" + response);
            buffer.clear();

        } catch (IOException e) {
            e.printStackTrace();
        }
        return response;
    }
}

 

EchoTest 

package org.example;

import org.junit.*;


import java.io.IOException;

import static org.junit.Assert.assertEquals;

public class EchoTest {
    Process server;
    EchoClient client;

    @Before
    public void setup() throws IOException, InterruptedException {
        server = EchoServer.start();
        client = EchoClient.start();
    }

    @Test
    public void givenServerClient_whenServerEchosMessage_thenCorrect(){
        String resp1 = client.sendMessage("hello");
        String resp2 = client.sendMessage("world");
        assertEquals("hello", resp1);
        assertEquals("world", resp2);
    }

    @After
    public void close() throws IOException {
        server.destroy();
        client.stop();
    }
}

tcpdump로 패킷 까보기

 

 

네트워크 트래픽은 네트워크를 통해 전송되는 데이터의 흐름이다. 서버와 클라이언트 간의 통신이 이루어질 때, 데이터는 여러 개의 작은 패킷으로 나뉘어 전송된다. tcpdump는 이 패킷들을 캡처하고, 캡처된 패킷이 담긴 덤프를 분석해서, TCP 통신에서 실제로 어떤 데이터가 오가는지 확인한다. . 애플리케이션 타임아웃의 원인을 분석할 때도 사용된다. 

 

아래 코드로 EchoClient 를 살짝 바꿔서 clientServer 를 띄운다.

 

public class EchoClientV2 {
    private static SocketChannel client;
    private static ByteBuffer buffer;
    private static EchoClientV2 instance;

    public static void main(String[] args) throws IOException {
        start();
        sendMessage("Hello");
        sendMessage("world");
        stop();
    }

    public static EchoClientV2 start() {
        if(instance == null)
            instance = new EchoClientV2();
        return instance;
    }

    public static void stop() throws IOException {
        client.close();
        buffer = null;
    }

    private EchoClientV2() {
        try {
            client = SocketChannel.open(new InetSocketAddress("localhost", 8080));
            buffer = ByteBuffer.allocate(256);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static String sendMessage(String msg) {
        buffer = ByteBuffer.wrap(msg.getBytes());
        String response = null;
        try {
            client.write(buffer);
            buffer.clear();
            client.read(buffer);
            response = new String(buffer.array()).trim();
            System.out.println("response=" + response);
            buffer.clear();

        } catch (IOException e) {
            e.printStackTrace();
        }
        return response;
    }
}

 

sudo tcpdump -i loO port 8080

 

로컬 호스트에서 포트 8080으로 들어오거나 나가는 모든 트래픽을 모니터링 할 수 있다.

 

-X 를 붙이면, ASCII 형식으로 볼 수 있다. 

sudo tcpdump -i lo0 port 8080 -X

 

위의 예제에서는 client → Server : Hello 메시지를 보냈고, Server가 echo 로서 그대로 응답한다.

아래 패킷에서도 확인할 수 있음.

 

Address already in use 일 때, 기존 Process 를 종료해보자.

현재 localhost:8080 에 Socket 을 바인드 하려고 하지만, 현재 다른 프로세스에 의해 사용 중이다는 뜻이다. 

 

프로세스 점유 중인 PID 찾기 : lsof -i :{port 번호} 

프로세스 죽이기 : kill -9 {PID}

CLI에서 netstat으로 포트와 PID 찾아서 kill 해보기

 

코드: https://github.com/jieun-dev1/Fundamentals/tree/feature/socket-communication

참고

https://www.cloudflare.com/ko-kr/learning/network-layer/what-is-a-packet/

https://kouzie.github.io/java/java-NIO/#java-nio

https://www.baeldung.com/java-nio-selector

https://www.lesstif.com/linux-core/unix-domain-socket

 

Comments