먼저 자바에서 소켓 통신은 C 또는 C++ 언어로 구현된 프로젝트와의 통신에 많이 사용됩니다.
이유는 Java와 C의 데이터 개념이 다르기 때문인데요. C에서는 구조체를 사용하는데 반해서 Java에는 구조체가 없습니다.
이처럼 Java의 Object 구조를 C에서 이해하지 못하고 C의 구조체를 자바에서 이해하지 못하기 때문에 서로 통신을 위해서는 byte 단위로 정보를 주고받아야 합니다.
(Socket을 사용한 전문 통신)
- 단방향 통신인 Http 통신
Http 통신은 Client의 요청(Request)이 있을 때만 서버가 응답(Response)하여 해당 정보를 전송하고 곧바로 연결을 종료하는 방식입니다. Client가 요청을 보내는 경우에만 Server가 응답하는 단방향 통신으로 반대로 Server가 Client에게 요청을 보낼 수는 없습니다.
- 양방향 통신인 Socket 통신
Server와 Client가 특정 Port를 통해 실시간으로 양방향 통신을 하는 방식입니다. Http 통신과는 다르게 Server와 Client가 특정 Port를 통해 연결되어 있어서 실시간으로 양뱡향 통신을 할 수 있습니다.
Streaming 중계나 실시간 채팅, 게임 등과 같이 즉각적으로 정보를 주고받는 경우에 사용됩니다.
아래 Socket 통신 예제 코드에서 보게 될 Stream에 대해서 간단하게 이야기하고 넘어가겠습니다.
Stream은 프로그램 동작 중 외부에서 데이터를 읽거나 외부로 데이터를 출력하는 작업에 사용됩니다. 이때 데이터는 어떤 통로를 통해서 이동되는데 이 통로를 Stream이라고 합니다.
자바에서는 외부에서 데이터를 읽는 역할을 수행하는 InputStream과 외부로 데이터를 출력하는 역할을 수행하는 OutputStream이 존재하며, 이 둘은 단일 방향으로 연속적으로 흘러갑니다.
(단방향이라는 특징 때문에 하나의 스트림으로 입출력을 동시에 할 수 없어서 InputStream과 OutputStream이 따로 존재합니다.)
소켓은 응용프로그램에서 TCP/IP를 이용하는 창구 역할을 하며, 두 프로그램이 네트워크를 통해 서로 통신을 수행할 수 있도록 양쪽에서 생성되는 링크의 단자입니다. 두 소켓이 연결되면 서로 다른 프로그램이 서로 데이터를 전달할 수 있게 됩니다.
이러한 Socket 통신은 일련의 규칙이 정해져 있는데요.
/*
Chatting 서버
1. 클라이언트의 접속 -> ServerWorker Thread 생성 및 start
2. ServerWorker -> 개별 client에 채팅 서비스
*/
public class ChatServer {
private ArrayList<ServerWorker> list = new ArrayList<ServerWorker>();
public void go() throws IOException {
ServerSocket serverSocket = null;
try {
// 채팅 서버 시작
serverSocket = new ServerSocket(5432);
System.out.println("**ChatServer Start**");
// 다수의 클라이언트에게 지속적으로 서비스하기 위해 while 이용
while (true) {
Socket socket = serverSocket.accept();
ServerWorker sw = new ServerWorker(socket);
list.add(sw);
Thread thread = new Thread(sw);
thread.start();
}
} finally {
if (serverSocket != null)
serverSocket.close();
System.out.println("**ChatServer End**");
}
}
public void sendMessage(String message) {
System.out.println(message);
// 접속해 있는 모든 클라이언트들에게 메세지 전송
for (int i=0;i<list.size();i++) {
list.get(i).pw.println(message);
}
}
class ServerWorker implements Runnable {
private Socket socket;
private BufferedReader br;
private PrintWriter pw;
private String user;
public ServerWorker(Socket socket) {
super();
this.socket = socket;
user = socket.getInetAddress().toString().replaceAll("/","");
}
public void chatting() throws IOException {
br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
pw = new PrintWriter(socket.getOutputStream(),true);
sendMessage(user+"님이 입장하셨습니다");
//로그 생성 영역 시작
LocalDateTime currentDateTime = LocalDateTime.now();
String filePath = "Test.txt";
File file = new File(filePath); // File객체 생성
if(!file.exists()){ // 파일이 존재하지 않으면
file.createNewFile(); // 신규생성
}
// BufferedWriter 생성
BufferedWriter writer = new BufferedWriter(new FileWriter(file, true));
//로그 생성 영역 끝
try {
while (true) {
String message = br.readLine();
if (message.trim().equals("종료") || message.equals("null") || message == null) {
break;
}
sendMessage(user+"님:"+message);
//여기다 로그 남기기
writer.write("[" + currentDateTime +"] " + "[" + user + "] " + "[" + message + "]");
writer.newLine();
writer.flush();
} // while
} // echo method
catch (Exception e) {
}
}
public void run() {
try {
chatting();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
closeAll();
} catch (IOException e) {
e.printStackTrace();
}
sendMessage(user+"님이 나가셨습니다!!");
list.remove(this);
}
}
public void closeAll() throws IOException {
if (pw != null)
pw.close();
if (br != null)
br.close();
if (socket != null)
socket.close();
}
}
public static void main(String[] args) {
try {
new ChatServer().go();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Server
데이터 송수신은 blocking 방식으로 동작합니다. 때문에 메인 스레드로만 구성하면 데이터를 올바르게 수신하는데 문제가 생길 수 있어 Multi-Thread 서버로 구현하였습니다.
* Thread 클래스는 start() 메서드 실행 시 run() 메서드가 수행되도록 내부적으로 동작합니다.
Server 코드에서는 'ServerSocket'과 'Socket' 두 가지 소켓을 볼 수 있는데요.
서버 소켓은 말 그대로 서버 프로그램에서 사용하는 소켓으로 ServerSocket 객체를 생성하여 클라이언트가 연결해오는 것을 기다립니다.
클라이언트가 연결해 올 때마다 요청은 요청 큐(Request Queue)에 쌓이고, 각각의 클라이언트 연결에 accept() 함으로써 요청을 요청 큐에서 꺼내고 Socket 객체가 리턴됩니다.
이렇게 리턴되는 Socket을 활용하여 클라이언트와 데이터를 주고받으며, 예시와 같은 멀티스레드(Multi-Thread) 환경에서는 Socket을 생성한 스레드에 주어서 클라이언트와 데이터를 주고받습니다.
Socket socket = ServerSocket.accept();
ServerSocket에서 내부적으로 동작하는 코드를 살펴보면 accept()를 통해 Socket 객체를 가지고 오는 부분에서 내부적으로 bind -> listen 과정이 실행됩니다.
/*
채팅 클라이언트
스레드 ( 세부적 실행단위 )
1. ReceiverWorker implements Runnable : 친구들의 메세지를 입력받는 역할
2. ChatClient의 main thread : ReceiverWorker Thread 생성 start
자신은 친구들에게 메세지를 출력하는 역할
*/
public class ChatClient {
private Socket socket;
private BufferedReader br;
private PrintWriter pw;
private Scanner sc;
public void go() throws UnknownHostException, IOException {
try {
socket = new Socket("127.0.0.1", 5432);
br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
pw = new PrintWriter(socket.getOutputStream(),true);
sc = new Scanner(System.in);
ReceiverWorker rw = new ReceiverWorker();
Thread thread = new Thread(rw);
thread.setDaemon(true);
thread.start();
System.out.println("**ChatClient가 서버에 접속**");
while (true) {
//System.out.print("서버에 보낼 메세지:");
String message = sc.nextLine();
pw.println(message);
if (message.trim().equals("종료")) {
System.out.println("**ChatClient 종료합니다**");
break;
}
}
}finally {
closeAll();
}
}
class ReceiverWorker implements Runnable {
public void run() {
try {
receiveMessage();
} catch (IOException e) {
e.printStackTrace();
}
}
public void receiveMessage() throws IOException {
while (true) {
String message = br.readLine();
if (message == null) {
break;
}
System.out.println(message);
System.out.print("서버에 보낼 메세지:");
}
}
}
public static void main(String[] args) {
ChatClient client = new ChatClient();
try {
client.go();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public void closeAll() throws IOException {
if (pw != null)
pw.close();
if (sc != null)
sc.close();
if (br != null)
br.close();
if (socket != null)
socket.close();
}
}
Client
Socket socket = new Socket("localhost", 포트번호);
Socket socket = new Socket(new InetSocketAddress("localhost", 포트번호));
Client에서 Socket을 사용하기 위한 두 가지 방법입니다. 연결하려는 외부 서버의 IP주소 대신 도메인 이름을 알고 있을 때 InetSocketAddress class를 사용할 수 있습니다.