1대1 소켓 프로그래밍에 대한 예제는 인터넷에 많기 때문에, TCP기반 비동기식 1대N 소켓 어플리케이션을 구현해보았다. 1대N은 1개의 서버가 N개의 클라이언트와 네트워킹 하는 걸 의미한다. 급한 사람은 바로 깃헙으로 가서 클론하여 사용하면 된다.
처음부터 끝까지 설명하면 지루해지기 때문에, 핵심만 집어서 설명하겠다.
비동기 양방향 네트워킹을 위해, 소켓 프로그래밍에선 '서버'와 '클라이언트'가 필요하다. 1대N 소켓을 구현하려면, 클라이언트보다는 서버가 할 일이 많다. 우선 여러 대의 클라이언트를 비동기로 처리하기 위해선 Thread 처리를 해야한다.
여기서 스레드 처리는 이중으로 해야하는데,
1) ConnectThread : 서버와 클라이언트를 연결한 스레드
2) ClientThread : 1)의 스레드를 기반으로 클라이언트가 활동할 수 있는 스레드
이 두개가 필요하다. Server.java라는 큰 범위에서 Connect를 먼저하고, 각각의 connect에 대해 클라이언트가 활동할 자리(스레드)를 만들어주는 느낌으로 이해하면된다.
스레드를 이중으로 구현해야하는 서버와 달리 반면 클라이언트는 서버에게 '나 이런 값 너한테 보낸다아~~~'를 보내고, 서버의 응답을 처리하면 끝이다.
Server.java
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Scanner;
class ClientThread extends Thread
{
Socket socket;
int id;
ClientThread (Socket socket, int id)
{
this.socket = socket;
this.id = id;
}
@Override
public void run ()
{
try
{
while (true)
{
InputStream IS = socket.getInputStream();
byte[] bt = new byte[256];
int size = IS.read(bt);
String output = new String(bt, 0, size, "UTF-8");
System.out.println("Thread " + id + " > " + output);
}
} catch (IOException e)
{
System.out.println(" Thread " + id + " is closed. ");
}
}
}
class ConnectThread extends Thread
{
ServerSocket serverSocket;
int count = 1;
ConnectThread (ServerSocket serverSocket)
{
System.out.println(Server.getTime() + " Server opened");
this.serverSocket = serverSocket;
}
@Override
public void run ()
{
try
{
while (true)
{
Socket socket = serverSocket.accept();
System.out.println(" Thread " + count + " is started.");
ClientThread clientThread = new ClientThread(socket, count);
clientThread.start();
count++;
}
} catch (IOException e)
{
System.out.println(" SERVER CLOSE ");
}
}
}
public class Server
{
public static void main (String[] args)
{
Scanner input = new Scanner(System.in);
ServerSocket serverSocket = null;
try
{ // 서버소켓을 생성, 7777 포트와 binding
serverSocket = new ServerSocket(7777); // 생성자 내부에 bind()가 있고, bind() 내부에 listen() 있음
ConnectThread connectThread = new ConnectThread(serverSocket);
connectThread.start();
int temp = input.nextInt(); // 스레드 생성 전에 숫자를 입력하면 바로 SERVER CLOSE!
} catch (IOException e)
{
e.printStackTrace();
}
try
{
serverSocket.close();
} catch (Exception e)
{
System.out.println(e);
}
}
static String getTime ()
{
SimpleDateFormat f = new SimpleDateFormat("[hh : mm : ss ]");
return f.format(new Date());
}
}
Client.java
import java.io.*;
import java.net.ConnectException;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
public class Client
{
public static void main (String[] args)
{
Scanner input = new Scanner(System.in);
String serverIp = "127.0.0.1";
System.out.println("Connecting to server. Server IP :" + serverIp);
try
{
// 소켓을 생성하여 연결을 요청한다.
Socket socket = new Socket(serverIp, 7777);
while (true)
{
System.out.println("> ");
String msg = input.nextLine();
byte[] arrayStream = msg.getBytes(StandardCharsets.UTF_8);
OutputStream outputStream = socket.getOutputStream();
outputStream.write(arrayStream);
}
} catch (ConnectException ce)
{
ce.printStackTrace();
} catch (IOException ie)
{
ie.printStackTrace();
} catch (Exception e)
{
e.printStackTrace();
}
}
}
build.gradle
plugins {
id 'application'
id 'java'
id 'idea'
}
group 'org.example'
version '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0'
}
test {
useJUnitPlatform()
}
startScripts.enabled = false
task Server(type: CreateStartScripts)
{
mainClass = 'Server'
applicationName = 'server'
outputDir = new File(project.buildDir, 'tmp')
classpath = startScripts.classpath
}
task Client(type: CreateStartScripts)
{
mainClass = 'Client'
applicationName = 'client'
outputDir = new File(project.buildDir, 'tmp')
classpath = startScripts.classpath
}
applicationDistribution.into('bin') {
from(Server)
from(Client)
fileMode = 0755
}