-
- 네트워킹(networking) : 네트워크 상에서 데이터를 주고 받는 것
- 전송용 프로토콜(protocol) : 데이터를 주고 받기 위해 정의된 규칙
- TCP(Transmission Control Protocol)
- 상대방이 연결이 되면 데이터 전송하는 방식 - UDP(User Datagram Protocol)
- 상대방과의 연결 여부와 상관없이 데이터를 전송하는 방식
- TCP(Transmission Control Protocol)
- TCP는 상대방 연결이 확인된 통신 회선을 고정하여 송수신한다.
- 데이터 손실이 적다.
- 보낸 데이터가 순서대로 전달된다. - 자바에서 TCP 네트워킹을 하기위해 ServerSocket과 Socket 클래스(java.net)를 제공한다.
- ServerSocket
- 클라이언트의 연결을 수락하는 서버 측 클래스
- IP 주소와 객체를 생성할 때 바인딩할 포트 번호를 지정해야 한다.
- 클라이언트와 연결이 되면 서버 측에 Socket 객체를 생성하여 클라이언트 측의 Socket을 통해 데이터 입출력을 수행한다. - Socket
- 클라이언트에서 연결 요청을 할 때와 클라이언트와 서버 양쪽에서 데이터를 주고 받을 때 사용되는 클래스
- ServerSocket
TCP 서버
자바에서 TCP 서버 프로그램 구축 순서
1. ServerSocket 객체 생성
//바인딩할 포트 번호('50001')를 지정하여 ServerSocket 객체 생성ServerSocket serverSocket = new ServerSocket(50001);//기본 생성자로 ServerSocket 객체를 생성하고 메소드를 호출하여 포트 번호 바인딩하는 방법ServerSocket serverSocket = new ServerSocket();serverSocket.bind(new InetSocketAddress(50001);//연결할 특정 서버 PC의 IP와 바인딩할 포트를 동시에 지정하는 방법//하나의 서버 컴퓨터에 여러 IP가 할당된 상태에서 특정 IP에서만 서비스할 때 사용ServerSocket serverSocket = new ServerSocket();serverSocket.bind( new InetSocketAddress("xxx.xxx.xxx.xxx", 50001) );2. 연결 요청 수락
- accept() 메소드를 실행
- 클라이언트가 연결 요청하기 전까지 블로킹(일시 정지)된다.
- 클라이언트의 연결 요청이 들어오면 블로킹이 해제되고 클라이언트와 통신할 수 있는 Socket 객체를 리턴
- 서버의 Socket에는 클라이언트의 연결 정보(IP 주소와 포트 번호)가 내장되어 있다.
//클라이언트와의 연결 요청 수락 및 통신을 위한 Socket 객체 생성Socket socket = serverSocket.accept();//Socket을 통해 연결된 클라이언트의 IP 주소와 Port 번호 추출InetSocketAddress isa = (Inet4Address) socket.getRemoteSocketAddress();String clientIp = isa.getHostName(); //클라이언트의 IP 주소String portNo = isa.getPort(); //클라이언트의 포트 번호3. 서버 종료
- 대부분은 특별한 이유 없이 서버를 종료하지 않음
- 유지보수 차원에서 기능을 추가/수정하거나 다른 프로그램을 실행할 때 기존 서버를 종료한다.
- close() 메소드를 호출하여 포트 번호 언바인딩해야 다른 프로그램에서 해당 포트 사용이 가능해진다.
//기존 서버 종료serverSocket.close();- 이미 다른 프로그램에서 사용 중인 포트 번호를 바인딩할 경우 BindException이 발생한다.
- 다른 포트 번호를 바인딩하거나 해당 포트를 사용하는 프로그램을 종료하고 다시 샐행해야 한다.
TCP 클라이언트
- 클라이언트가 서버에 연결 요청하려면 Socket 객체를 생성해야 한다.
- 매개 값으로 서버 IP 주소와 Port 번호를 입력하여 생성하면 Socket 객체를 생성하는 동시에 서버 연결 요청이 발생한다.
//클라이언트의 Socket 객체 생성(서버 IP를 알고 있는 경우)Socket socket = new Socket("IP", 50001);//로컬 컴퓨터의 서버로 연결 요청할 경우Socket socket = new Socket("localhost", 50001);//도메인 이름으로 서버 연결 요청할 경우Socket socket = new Socket(InetAddress.getByName("domainName"), 50001); //DNS 서버에 접속해서 도메인에 해당되는 IP를 먼저 언어옴.//기본 생성자로 Socket 객체 먼저 생성한 후 나중에 서버와 연결하는 방법Socket socket = new Socket();socket.connet( new InetSocketAddress("domain", 50001) );- 서버 연결 요청 시 발생할 수 있는 예외에 대한 예외 처리 필요
- UnkownHostException
- 잘못된 IP 주소 입력 시 발생 - IOException
- 제공된 IP 주소와 포트 번호로 연결할 수 없을 때 발생
- UnkownHostException
- 클라이언트와 서버 연결 종료
- 연결된 Socket 객체에서 close() 메소드 실행
- 클라이언트, 서버 양쪽에서 연결 종료 가능
//클라이언트 연결 종료socket.close();
입출력 스트림으로 데이터 송수신
- 클라이언트가 연결 요청하고 서버가 연결 수락을 하면 Socket 객체로부터 입력 스트림과 출력 스트림을 통해 데이터를 주고 받을 수 있다.
- InputStream과 OutputStream은 Socket에서 제공해준다.
- InputStream
- 서버/클라이언트로부터 데이터를 받을 때 사용 - OutputStream
- 클라이언트/서버로 데이터를 보낼 때 사용
- InputStream
//Socket을 통한 입력 스트림 얻기InputStream is = socket.getInputStream();//Socket을 통한 출력 스트림 얻기OutputStream os = socket.getOutputStream();
출력 스트림을 이용한 데이터 송신
//UTF-8로 인코딩하여 바이트 배열을 얻고 상대방에게 전달String data = "전송 데이터";byte[] bytes = data.getByBytes("UTF-8");OutputStream os = socket.getOutputStream();os.write(bytes);os.flush();- 일반 입출력 스트림과 동일하게 보조 스트림을 추가로 연결할 수도 있다.
//문자열 데이터를 간편하게 전달하는 보조스트림을 연결한 방법String data = "전송 데이터";DataOutputStream dos = new DataOutputStream(socket.getOutputStream());dos.writeUTF(data);dos.flush();
입력 스트림을 이용한 데이터 수신
- InputStream을 이용해 특정 문자셋으로 디코딩하여 데이터 수신 가능
//UTF-8로 디코딩하여 바이트 배열에 데이터 저장byte[] bytes = new byte[1024];InputStream is = socket.getInputStream();int num = is.read(bytes);String data new String(bytes, 0, num, "UTF-8"); //바이트 배열에 저장된 데이터를 인덱스 0부터 읽어온 부분까지 UTF-8로 디코딩하여 문자열로 변환- DatatInputStream 보조 스트림을 이용한 데이터 읽기는 상대방이 DataOutputStream으로 문자영을 보낼 경우에만 가능하다.
//보다 간편하게 데이터를 읽어오기 위해 DataInputStream 사용한 방법DataInputStream dis = new DataInputStream(socket.getInputStream());String data = dis.readUTF();
클라이언트에서 보낸 메시지를 재전송 하는 에코 프로그램
- 서버 프로그램을 먼저 실행
- 클라이언트 프로그램을 실행
- 클라이언트 프로그램은 한 번의 실행에 메시지를 한 번 송신, 수신하고 종료됨
- 클라이언트를 여러 번 실행하면 서버 쪽 콘솔에 연결 및 데이터 송수신 내역이 모두 기록됨
서버 프로그램 소스코드
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103/** 클라이언트에서 보낸 메시지를 받아서 다시 클라이언트로 보내는 에코(메아리) 프로그램*/public class EchoServer {private static ServerSocket serverSocket;public static void main(String[] args) {System.out.println("------------------------------------------------------------");System.out.println("서버를 종료하려면 q 또는 Q를 입력하고 Enter키를 입력하세요.");System.out.println("------------------------------------------------------------");//TCP 서버 시작startServer();//키보드 입력Scanner scanner = new Scanner(System.in);while(true) {String key = scanner.nextLine();if(key.toLowerCase().equals("q")) { //대소문자 구분없이 인식break;}}scanner.close();//TCP 서버 종료stopServer();}private static void startServer() {//작업 스레드 정의Thread thread = new Thread() {@Overridepublic void run() {//ServerSocket 생성 및 Port 바인딩try {serverSocket = new ServerSocket(50001);System.out.println("[서버] 시작됨");//여러 클라이언트의 연결 요청을 수락하기 위해 요청 수락을 반복while(true) {System.out.println("\n[서버] 연결 요청을 기다림\n");//연결 수락Socket socket = serverSocket.accept();//연결된 클라이언트 정보 얻기InetSocketAddress isa = (InetSocketAddress) socket.getRemoteSocketAddress();String clientIp = isa.getHostName();// String clientIp = isa.getHostString();System.out.println("[서버] " + clientIp + "의 연결 요청을 수락함");// //데이터 수신// InputStream is = socket.getInputStream();// byte[] bytes = new byte[1024];// int readByteCount = is.read(bytes);// String message = new String(bytes, 0, readByteCount, "UTF-8");// //데이터 송신// OutputStream os = socket.getOutputStream();// bytes = message.getBytes("UTF-8");// os.write(bytes);// os.flush();// System.out.println("[서버] 받은 데이터를 다시 보냄: " + message);//데이터 수신DataInputStream dis = new DataInputStream(socket.getInputStream());String message = dis.readUTF(); //UTF-8로 변환하여 읽기//데이터 송신DataOutputStream dos = new DataOutputStream(socket.getOutputStream());dos.writeUTF(message); //UTF-8로 인코딩하여 전달dos.flush();System.out.println("[서버] 받은 데이터를 다시 보냄: " + message);//연결 끊기socket.close(); //Socket close() 시 Socket을 통해 얻은 입출력 스트림도 colse됨System.out.println("[서버] " + clientIp + "의 연결을 끊음 ");}} catch (IOException e) {System.out.println("[서버] " + e.getMessage()); //예외 처리로 안전하게 종료// e.printStackTrace(); //어느 부분에서 예외가 발생했는지 표시}}};//스레드 시작thread.start();}private static void stopServer() {//ServerSocket을 닫고 Port 언바인딩try {serverSocket.close();} catch (IOException e) {e.printStackTrace();}}}클라이언트 프로그램 소스코드
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647public class EchoClient {public static void main(String[] args) {try {Socket socket = new Socket("localhost", 50001);System.out.println("[클라이언트] 연결 성공");// //데이터 송신// String sendMessage = "나는 자바가 좋아~~";// OutputStream os = socket.getOutputStream();// byte[] bytes = sendMessage.getBytes("UTF-8");// os.write(bytes);// os.flush();// System.out.println("[클라이언트] 데이터 보냄: " + sendMessage);//// //데이터 받기// InputStream is = socket.getInputStream();// bytes = new byte[1024];// int readByCount = is.read(bytes);// String receiveMessage = new String(bytes, 0, readByCount, "UTF-8");// System.out.println("[클라이언트] 데이터 받음: " + receiveMessage);//데이터 송신String sendMessage = "나는 자바가 좋아~~";DataOutputStream dos = new DataOutputStream(socket.getOutputStream());dos.writeUTF(sendMessage);dos.flush();System.out.println("[클라이언트] 데이터 보냄: " + sendMessage);//데이터 받기DataInputStream dis = new DataInputStream(socket.getInputStream());String receiveMessage = dis.readUTF();System.out.println("[클라이언트] 데이터 받음: " + receiveMessage);socket.close();System.out.println("[클라이언트] 연결 끊음");} catch (UnknownHostException e) {//IP 또는 도메인 표기 방법이 잘못되었을 경우System.out.println("UnknownHostException: " + e.toString());} catch (IOException e) {//IP 또는 Port 번호가 존재하지 않을 경우System.out.println("IOException: " + e.toString());}}}실행 결과
TCP 채팅 프로그램
- TCP 네트워킹을 이용해서 채팅 서버와 클라이언트를 구현할 수 있다.
- 클래스 구성
- ChatServer
- 채팅 서버 실행 클래스
- ServerSocket을 생성하고 50001 포트 번호로 바인딩
- ChatClient 연결 수락 후 SocketClient 생성 - SocketClient
- ChatClient와 1대1 통신
- 서버 쪽에서 통신을 하는 객체
- ChatClient와 1대1로 매핑되면서 통신 - ChatClient
- 채팅 클라이언트 실행 클래스
- ChatServer에 연결 요청
- SocketClient와 1대1로 통신
- ChatServer
채팅 서버
- 채팅 서버 실행 클래스인 ChatServer는 클라이언트의 연결 요청을 수락하고 통신용 SocketClient를 생성하는 역할을 수행한다.
ChatServer
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103import java.io.IOException;import java.net.ServerSocket;import java.net.Socket;import java.util.Collection;import java.util.Collections;import java.util.HashMap;import java.util.Map;import java.util.Scanner;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import org.json.JSONObject;public class ChatServer {//필드ServerSocket serverSocket;ExecutorService threadPool = Executors.newFixedThreadPool(100);Map<String, SocketClient> chatRoom = Collections.synchronizedMap(new HashMap<String, SocketClient>());//메소드public void start() throws IOException {serverSocket = new ServerSocket(50001);System.out.println("[서버] 시작됨");Thread thread = new Thread(() -> {try {while(true) {Socket socket = serverSocket.accept();SocketClient sc = new SocketClient(this, socket);}} catch(Exception e) {}});thread.start();}public void addSocketClient(SocketClient socketClient) {String key = socketClient.chatName + "@" + socketClient.clientIp;chatRoom.put(key, socketClient);System.out.println("입장: " + key);System.out.println("현재 채팅자 수: " + chatRoom.size() + "\n");}public void removeSocketClient(SocketClient socketClient) {String key = socketClient.chatName + "@" + socketClient.clientIp;chatRoom.remove(key);System.out.println("나감: " + key);System.out.println("현재 채팅자 수: " + chatRoom.size() + "\n");}public void sendToAll(SocketClient sender, String message) {JSONObject root = new JSONObject();root.put("clientIp", sender.clientIp);root.put("chatName", sender.chatName);root.put("message", message);String json = root.toString();Collection<SocketClient> socketClients = chatRoom.values();for(SocketClient sc : socketClients) {if(sc == sender) continue;sc.send(json);}}public void stop() {try {serverSocket.close();threadPool.shutdown();chatRoom.values().stream().forEach(sc -> sc.close());} catch (IOException e) {}System.out.println("[서버] 종료됨");}public static void main(String[] args) {try {ChatServer chatServer = new ChatServer();chatServer.start();System.out.println("------------------------------------------------------------");System.out.println("서버를 종료하려면 q 또는 Q를 입력하고 Enter키를 입력하세요.");System.out.println("------------------------------------------------------------");//키보드 입력Scanner scanner = new Scanner(System.in);while(true) {String key = scanner.nextLine();if(key.toLowerCase().equals("q")) {break;}}scanner.close();//TCP 서버 종료chatServer.stop();} catch (IOException e) {e.printStackTrace();}}}SocketClient
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980import java.io.DataInputStream;import java.io.DataOutputStream;import java.io.IOException;import java.net.InetSocketAddress;import java.net.Socket;import org.json.JSONObject;public class SocketClient {//필드ChatServer chatServer;Socket socket;String clientIp;String chatName;DataInputStream dis;DataOutputStream dos;//생성자public SocketClient(ChatServer chatServer, Socket socket) {try {this.chatServer = chatServer;this.socket = socket;this.dis = new DataInputStream(socket.getInputStream());this.dos = new DataOutputStream(socket.getOutputStream());InetSocketAddress isa = (InetSocketAddress) socket.getRemoteSocketAddress();this.clientIp = isa.getHostName();receive();} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}}//클라이언트로부터 메시지를 받기 위한 메소드private void receive() {chatServer.threadPool.execute(() -> {try {while(true) {//{ "command": "incoming", "data": "chatName" }//{ "command": "message", "data": "xxxx" }String receiveJson = dis.readUTF();JSONObject jsonObject = new JSONObject(receiveJson);String command = jsonObject.getString("command");switch(command) {case "incoming":this.chatName = jsonObject.getString("data");chatServer.sendToAll(this, "들어오셨습니다.");chatServer.addSocketClient(this);break;case "message":String message = jsonObject.getString("data");chatServer.sendToAll(this, message);break;}}} catch(IOException e) {chatServer.sendToAll(this, "나가셨습니다.");chatServer.removeSocketClient(this);}});}//클라이언트로 메시지를 보내기 위한 메소드public void send(String json) {try {dos.writeUTF(json);dos.flush();} catch (IOException e) {}}public void close() {try {socket.close();} catch (IOException e) {}}}
채팅 클라이언트
- 채팅 서버로 연결 요청(TCP 방식)하고, 연결된 후에는 대화명을 보낸 다음 서버와 메시지를 주고 받는다.
ChatClient
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394import java.io.DataInputStream;import java.io.DataOutputStream;import java.io.IOException;import java.net.Socket;import java.util.Scanner;import org.json.JSONObject;public class ChatClient {//필드Socket socket;DataInputStream dis;DataOutputStream dos;String chatName;public void connect() throws IOException {socket = new Socket("localhost", 50001);dis = new DataInputStream(socket.getInputStream());dos = new DataOutputStream(socket.getOutputStream());System.out.println("[클라이언트] 서버에 연결됨");}public void receive() {Thread thread = new Thread(() -> {try {while(true) {String json = dis.readUTF();JSONObject root = new JSONObject(json);String clientIp = root.getString("clientIp");String chatName = root.getString("chatName");String message = root.getString("message");System.out.println("<" + chatName + "@" + clientIp + "> " + message);}} catch(IOException e) {System.out.println("[클라이언트] 서버에 연결 끊김");System.exit(0);}});thread.start();}public void send(String json) throws IOException {dos.writeUTF(json);dos.flush();}public void unconnect() throws IOException {socket.close();}public static void main(String[] args) {try {ChatClient chatClient = new ChatClient();chatClient.connect();Scanner scanner = new Scanner(System.in);System.out.print("대화명 입력: ");chatClient.chatName = scanner.nextLine();JSONObject jsonObject = new JSONObject();jsonObject.put("command", "incoming");jsonObject.put("data", chatClient.chatName);String json = jsonObject.toString();chatClient.send(json);chatClient.receive();System.out.println("------------------------------------------------------------");System.out.println("보낼 메시지를 입력하고 Enter");System.out.println("서버를 종료하려면 q 또는 Q를 입력하고 Enter키를 입력하세요.");System.out.println("------------------------------------------------------------");while(true) {String message = scanner.nextLine();if(message.toLowerCase().equals("q")) {break;} else {jsonObject = new JSONObject();jsonObject.put("command", "message");jsonObject.put("data", message);json = jsonObject.toString();chatClient.send(json);}}scanner.close();chatClient.unconnect();} catch(Exception e) {System.out.println("[클라이언트] 서버 연결 안됨");}System.out.println("[클라이언트] 종료");}}실행 결과
서버
클라이언트 1
클라이언트 2
- 프로그램 종료 테스트하다가 캡쳐 못함
클라이언트 3