파트
- 네트워크 기초
- IP 주소 얻기
- TCP
- UDP
- 서버의 동시 요청 처리
- JSON 데이터 형식
- TCP 채팅 프로그램
네트워크 기초
네트워크 Network
정점(or 버텍스 or 노드) + 간선(or 엣지 or 아크) 으로 "전자 제품"들이 "소통"할 수 있게 연결한 그래프
그래프 자료구조이기에 반드시 상위 루트가 존재하는 트리 같은 시스템이 아니고 그래프 조건만 만족하면 된다!
반드시 CPU + 버스 + 하드웨어 기본 골자를 담은 컴퓨터여야만 네트워크라고 하는건 아니다! USB 연결도 나름 네트워크지..
하지만 일반적으로 CPU + 랜카드 형태의 인터넷 네트워크를 다룬다고 생각하고 공부해 보았다.
LAN: Local Area Network
특정 지역에 국한하여 존재하는 컴퓨터들을 연결한 네트워크이다. 상대적으로 작은 네트워크라고 보면 되겠다. 타고타고 따라가다보면 결국 물리적으로 모든 애들이 너 누구지 너 누구네 이런식으로 바로 바로 추적이 됨.
WAN: Wide Area Network
LAN과 LAN을 연결하여 넓은 개념의 네트워크. 인터넷이란 개념을 구현하기 위해서는 WAN으로 인터넷 컨셉을 구현함.
오해방지
LAN과 LAN을 연결하면 무조건 WAN이 되는게 아니다! 물리적으로 중간 라우터를 물리고 하위를 허브로 구성할 경우가 그 예시인데 물리적으로 멀찍이 떨어져 있다고 해도!! 1동 아파트 호수와 1키로 떨어져 있는 2동 아파트 호수라고 해도 이런식으로 구성되면 물리적으로 커다란 LAN을 만드는것과 다를바가 없다.
WAN은 좀 더 플랫폼적이라 생각해도 좋다. 어떤 플랫폼이냐 ISP를 거쳐서 LAN과 LAN을 연결한 시스템이 WAN이다. WAN네트워크와 LAN네트워크는 실제 주소부터 다르다 ㅋㅋ 중개역할을 해줘야되다보니 다른 주소를 부여해주는거다 LAN에게. 이게 어 예시...
참고
LAN: 랜(라우터) <-> 랜(라우터) == 큰 랜(큰 라우터)
WAN: 랜(라우터) <-> 모뎀(아파트 인터넷 단자함에 있는 커다란 그것) <-> ISP회선<-> ISP <-> Internet <-> ISP <-> ISP포트 <-> 모뎀 <-> 라우터
ISP라는 녀석과 연결되어 있냐 아니냐로 WAN(인터넷)에 랜카드가 속하냐 아니냐를 결정할 수 있을듯.
ISP는 Internet Service Provider 이다. kt, comcast, softbank 등등
서버와 클라이언트
서버: Server
요청에 응답하는 자
클라이언트: Client
요청을 하는 자
IP 주소
정의: 컴퓨터의 최종 주소. xxx.xxx.xxx.xxx 총 4개의 xxx 집합이 .으로 구별되어져 있음. 각 xxx 그룹은 0~255사이 값을 가진다. 컴퓨터의 최종 주소라고 한다면 서버도 주소가 있고 클라이언트도 주소가 있다. 그리고 그 주소는 고유하다!
주의
랜카드마다 독립적으로 주소가 할당됨. 한컴퓨터가 랜카드 여러개면 주소가 여러개 일 수 있음.
도메인명: Domain Name
서버의 최종주소가 숫자로 복잡하면 인터넷 누가 써... 그래서 ICANN에 도메인명 레지스트리를 등록하여 관리하고 있다. 당연히 해당 도메인명은 고유한 키값이다. www.naver.com www.google.com 등이 유효한 도메인 명의 예시임. 그런데 ICANN의 역할은 그저 DB같은 역할이다. 고유한 키값을 저장하는건데... 저 키에 대응되는 xxx.xxx.xxx.xxx를 누가 가져오느냐? DNS가 가져온다.
DNS: Domain Name System
등록된 DomainName 키를 받으면 등록된 xxx.xxx.xxx.xxx로 반환해주는 시스템을 의미한다.
이런 시스템이 깔려있는 Server가 DNS Server이고, ISP 서비스 회사들은 자체 DNS Server를 운영하고 있다.
사용자 운영체제에는 자동으로 DNS Server 주소가 할당되어져 있고(WAN포트로 ISP회선과 연결되면 그때 라우터에 뿌려지고 라우터와 연결된 랜카드가 그 정보를 수신하여 DNS Server 주소를 세팅함...) 브라우저에서 Domain Name을 이용한 요청시도를 할때 DNS Server에 한번 가서 IP세팅을 하고 인터넷 세상으로가서 해당 IP로 서버를 검색하여 찾아낸뒤에 응답을 가져오는 형태가 전반적으로 돌아감.
통찰
서버 - 클라이언트가 소통을 하기 위해서는 서버는 항상 켜져 있어야함. 클라이언트는 지가 하고 싶은대로 요청을 할거거든
클라이언트의 요청은 IP 패킷의 형태로 출발지IP(클라IP)와 도착IP를 둘다 담아서 전달하고, 전달받은 서버는 패킷에있는 출발지IP정보를 참고하여 응답하는거.
여기서 궁금증이 들어야 정상. 기본적으로 나의 IP가 192.168.1.1 이라고 하면, 해당 IP를 가진 랜카드가 이 세상에 100만개는 넘지 않을까? 대체 IP 주소만으로 어떻게 나에게만 응답을 해주는거지??? 경우의 수가 애초에 43억개 밖에 안됨. 문제는 인구수 당 개인 네트워크는 물론이고, 랜카드가 여러개 쓰일 수 있으며, 회사 서비스 서버의 주소는 또 어쩔건데... 절대 네버 감당 안됨.
심화
이제 책에도 안나오는 내용을 적어야 할때가 왔다..
랜카드<->(허브)<->공유기(라우터)<->아파트 모뎀<->ISP 회선
여기서 각각의 IP주소는 고유한 로컬 IP 주소임. 글로벌이고 자시고 생각하지 말고 일단 LAN 시스템에서는 절대 안겹치게 자동으로 구성되니까 안심하시고..
라우터 or 모뎀 <-> ISP
여기사이에서 특별한 기술이 들어감. NAT: Network Address Translation 이라는 기술인데, ISP 회사들이 등록하여 관리하고 있고, 회사별, 지역별로 다르게 세팅되며 해당 기술로 변환된 IP를 공인IP라고 함. 인터넷에서 바로 이 공인 IP가 고유한 키값이기 때문에 글로벌한 인터넷 세상에서 고유한 지점에 접근가능하게 만들어줌
무수히 많은 랜세상 <-> 적당히 많은 ISP 세상 <-> 인터넷
이런식으로 서로 다르게 끊어져 있는 느낌이라, 경우의 수 컨트롤이 가능한거
??? 그런데 이것만으로도 안됨 ㅋㅋ 보면 IPv4의 최대 경우의 수는 43억개이다. 과연 43억개의 공인 IP주소만으로 전세계 글로벌 고유 키를 지역별 인터넷 회사별로 할당이 가능할까???
-> 사실… 인터넷을 가입하면.. 인터넷 가입자마다 고유 공인 IP 주소를 줘야됨. 그리고 실제로 그렇게 하고 있음. 근데 그렇게 되면,, 인터넷 여려개 가입해야되는 사업장이나 혹은 개인의 경우가 늘어날경우 감당이 안된다… 그래서 나오는게 CGNAT: Carrier-Grade NAT
로컬IP<->공인IP:포트<->공인IP<->인터넷
이 시스템을 도입하여 고갈된 공인 IP 키값을 막아내고 있다. 포트까지 더해지니 일단 경우의 수는 상당히 확보된 셈.
IPv6를 글로벌적으로 교체하려고 하는데 아직 그 비용 감당보단 포트로 때우는게 일반적이라 저렇게 처리되고 있는것으로 보인다
Port Number
위에서 말했지만, 개념은 컴퓨터의 주소인 IP는 도로명 주소 느낌이고, Port는 몇호실 인지를 나타내는 느낌이다. CGNAT에서는 컴퓨터들 그 자체를 호단위로 나눈건데 암튼 개념상 Port는 IP주소로 가서 구체적으로 어디에 접근하면 되는지를 설명하는 개념이다.
실제 서버에는 여러 프로그램이 돌고 있을 수가 있다. 그 여러 프로그램마다 각각의 고유한 Port를 설정하여 서버프로그래밍이 완성됨. 0~65535 까지 지정됨.
범위에 따라 개념적 의미는 나눠지지만..강제된 기술적의미를 가지지는 않는다.
Well Know Port Numbers : 국제인터넷 주소 관리 기구가 먼저 선점하여 예약한 포트
Registered Port Numbers : 회사에서 등록하여 사용가능한 포트
Dynamic Or Private Port Numbers : 운영체제가 사용하는 Port 및 개인 목적으로 사용가능
숫자까지 외우고 있진 말자. 힘들다
IP 주소 얻기
개요
IP 주소를 담기위한 전용 객체가 자바에 존재해요. 스태틱 메서드도 쓰고 필드도 있고 다양하게 짬뽕이요
클래스의 정체
public sealed class InetAddress implements Serializable permits Inet4Address, Inet6Address
//객체 생성하여 담기
InetAddress ia = InetAddress.getLocalHost(); //내 컴퓨터 로컬IP객체 담기
InetAddress ia2 = InetAddress.getByName(String domin); //도메인 공인 IP객체 담기
InetAddress[] ia3 = InetAddress.getAllByName(String domin); //도메인 공인 IP객체 리스트
//실제 IP주소 담기
String ip = ia.getHostAddress();
String ip2 = ia2.getHostAddress();
인터넷 프로토콜
개요
무리없이 브라우저에서 언제나 같은 방식으로 주소창에 입력하면 어련히 알아서 서버의 프로그램에 접속이 가능한건 규약이 있기 때문이다.
프로토콜: Protocol
규약, 규칙
인터넷 프로토콜 스위트: Internet Protocol Suite
인터넷환경에서 서로 정보를 주고 받는데 쓰이는 통신규약의 모음이다.
인터넷 프로토콜 스택: Internet Protocol Stack
프로그래밍에서 작업은 메모리 스택에 들어오고, 다시 메모리 스택을 반환하면서 즉 작업이 쌓인걸 해결해나가면서 프로그램은 돌아간다. 인터넷 작업도 마찬가지이다. 그런데 그 처리 방식을 계층으로 나눈게 인터넷 프로토콜 스택이다.
- 응용계층: Application Layer
- 전송계층: Transport Layer
- 인터넷 계층: Internet Layer
- 네트워크 접근 계층: Network Access Layer
하지만 이런 4계층으로 나눈것은 실상 TCP/IP 만 제대로 설명하기 위하여 간추렸다는 평이 많다. 그런데 네트워크 웹환경에서는 특히나 TCP/IP를 사용하기에 이것으로 간략화 해도 무방하다.
- 응용계층: HTTP
정의: 응용 프로그램이 네트워크를 이용할때 사용하는 프로토콜의 집합이 응용 계층 집합임. 응용 프로그램이다 보니 사용자가 직접 조작하는 어플리케이션이기에 사용자의 요청에의해 가장 먼저 구성되는 계층이라 1빠따이다. 브라우저(HTTP), 메일클라이언트(SMTP) 등이 네트워크를 사용하는 대표적인 클라이언트 응용 프로그램.
HTTP: 웹 사이트(페이지를 띄운 브라우저)에서 통신하는 기본 규약임. HyperText Transfer Protocol
담당: 브라우저가 대표적으로 HTTP 프로토콜을 관리하여 담당한다. 특히 요청에대한 정의가 이미 내려져 있다.
요청: GET, HEAD, POST, PATCH, PUT, DELETE, TRACE, CONNECT, OPTIONS
요청에대한 상세는 필요할때 알아보면 됨. url에 단순 검색하는건 GET 요청이라고 생각하면 된다. 그정도 - 전송계층: TCP
정의: How transfer 수신자 송신자간에 어떤 방식으로 전송할건데?에대한 규약들의 집합. 대표적으로 TCP UDP 방식이 있음
TCP: 연결형 프로토콜. 연결을 관리하기에 무손실을 보장한다. 연결을 통해 성공 여부를 체크하기 때문.
여기까지만.. - 인터넷 계층: IP
정의: What transfer 어떤 정보를 통신할지 - 네트워크 접근 계층: Ethernet
정의: IEEE 802.3 통신 규약. 신호와 배선 방식, MAC 패킷 등을 정의
정신나갈거 같다 여까지.. 이것이 자바다에선 여까지만하고.. 다른 네트워크 책을 한번 보긴 해야된다...
TCP 채팅
개요
암튼 열심히 위에 설명한걸 토대로 하면 크고 무겁지만, 연결 상태를 유지하는 방식 덕분에 무손실을 보장한다는 점.
TCP 서버
소켓을 활용한 네트워크 프로그래밍에서는 역시 담당할 객체가 필요하다. 서버에서는 크게 2가지가 있다. ServerSocket 객체와 Socket 객체이다.
ServerSocket
클래스의 정체
public class ServerSocket implements java.io.Closeable
생성하기
public ServerSocket(int port) throws IOException {
this(port, 50, null);
}
먼저 미리 개발자가 정의해두는 방식이 있다. 내 응용프로그램에서 port번호로 소켓 연결을 시도하면 우리 ServerSocket이 마주해줄거야. 라고 정의하는거다.
받을 준비하고 있다가 받으면 Socket 반환
public Socket accept() throws IOException {
if (isClosed())
throw new SocketException("Socket is closed");
if (!isBound())
throw new SocketException("Socket is not bound yet");
Socket s = new Socket((SocketImpl) null);
implAccept(s);
return s;
}
서버 프로그램의 스레드는 오픈 이래로 메인스레드가 되었든, 작업 서브스레드가 되었든 해당 accept() 매서드를 무한 반복 돌리고 있어야된다. 무한 반복 돌려도 호출될 일은 없다. 블로킹 처리가 되어 있다. 블로킹 처리에대한 구현은 OS 레벨의 네트워크 소켓 API가 담당하고 있다. 그래서 그게 뭐냐고? OS 수준이라는 말은 JNI(Java Native Interface)가 볼수 있는 최소 마지노선이며, 해당 인터페이스는 C언어로 된 네이티브 코드를 사용하기위한 인터페이스이다. 이 이상은 자바의 영역이 아니라는 소리다.
네트워크 이론을 배우면 알게될거 같은데 TCP - IP - Ithernet 으로 이어지는 영역 자체가 애초에 OS + 하드웨어에서 담당하기도 하니 OS 코드를 쓰는거 아닐까 싶다.
TCP 클라이언트
ServerSocket은 서버에만 있는 다리를 놔주는 녀석이고, 실제 데이터 송수신과 관련한 메서드들은 Socket 클래스에 있고 서버와 클라이언트는 이 Socket 객체를 통해 송수신한다. 차이점은... 서버는 연결된 클라이언트 수만큼 최소 Socket 객체를 가져야하고, 클라이언트 또한 연결된 서버의 수만큼 최소 Socket 객체를 가져야한다.
클래스의 정체
public class Socket implements java.io.Closeable
생성하기
@SuppressWarnings("this-escape")
public Socket(String host, int port)
throws UnknownHostException, IOException
{
this(host != null ? new InetSocketAddress(host, port) :
new InetSocketAddress(InetAddress.getByName(null), port),
(SocketAddress) null, true);
}
@SuppressWarnings("this-escape")
public Socket(InetAddress address, int port) throws IOException {
this(address != null ? new InetSocketAddress(address, port) : null,
(SocketAddress) null, true);
}
내부적으로 또 InetSocketAddress를 생성하여 생성자를 다시 만든다. 그런데 이런 자세한건 몰라도 된다. 그저 InetAddress 혹은 직접 도메인 명을 String으로 매개변수1에 넣어주면 된다.
이러면 자동으로 connect 시도까지 들어가게 된다.
private Socket(SocketAddress address, SocketAddress localAddr, boolean stream)
throws IOException
{
Objects.requireNonNull(address);
assert address instanceof InetSocketAddress;
// create the SocketImpl and the underlying socket
SocketImpl impl = createImpl();
impl.create(stream);
this.impl = impl;
this.state = SOCKET_CREATED;
try {
if (localAddr != null) {
bind(localAddr);
}
connect(address);
} catch (Throwable throwable) {
closeSuppressingExceptions(throwable);
throw throwable;
}
}
위의 생성자가 결국 호출되는건데... 중간에 보면 bind connect 이런애들이 바로 low-code로 접근하여 ServerSocket의 블로킹을 해제하는 매서드이다...
입출력 스트림으로 데이터 주고 받기
입출력 스트림을 얻어야 가져오고 넣어주고가 될거 아닌가. 그런데 어디로 부터 입출력할건가? 바로 상대방에게 입출력해야된다. 그 상대방에대한 정보를 어디서 가져오냐? 연결이 성공한 Socket 클래스이다..
public InputStream getInputStream() throws IOException {
int s = state;
if (isClosed(s))
throw new SocketException("Socket is closed");
if (!isConnected(s))
throw new SocketException("Socket is not connected");
if (isInputShutdown(s))
throw new SocketException("Socket input is shutdown");
InputStream in = this.in;
if (in == null) {
// wrap the input stream so that the close method closes this socket
in = new SocketInputStream(this, impl.getInputStream());
if (!IN.compareAndSet(this, null, in)) {
in = this.in;
}
}
return in;
}
public OutputStream getOutputStream() throws IOException {
int s = state;
if (isClosed(s))
throw new SocketException("Socket is closed");
if (!isConnected(s))
throw new SocketException("Socket is not connected");
if (isOutputShutdown(s))
throw new SocketException("Socket output is shutdown");
OutputStream out = this.out;
if (out == null) {
// wrap the output stream so that the close method closes this socket
out = new SocketOutputStream(this, impl.getOutputStream());
if (!OUT.compareAndSet(this, null, out)) {
out = this.out;
}
}
return out;
}
더 딥하게 들어가면 SocketInputStream SocketOutputStream인데 굳이 알필요 없다. 소켓을 해석하는거를 담고 있겠지. 중요한건 내 메시지를 보내기 위한 놈 상대 메시지를 가져오기 위한 놈이다.
UDP 소켓
개요
UDP: User Datagram Protocol
진짜 오히려 찐 데이터를 송수신하는건 이 녀석일지 모른다. statless이다 연결되고 있는지 자시고를 계속 체크하고있지않는다. 성공적으로 요청받는순간 그저 작동하여 응답할 뿐이다.
클래스 구조는 서버 클라 구별없이 DatagramSocket이 DatagramPacket 객체를 이용하여 수신하고 발신한다.
UDP Server
개요
클라이언트의 요청을 받는 역할을 해야되는건 동일하다. 그러기 위해선 Port 바인딩은 필수다. 요청을 할 주소를 명확히 정의해줘야되니까.
서버는 포트를 바인딩
public DatagramSocket(int port) throws SocketException {
this(port, null);
}
클라이언트의 요청을 항시 받을 준비를하고 있어야할때 사용하는 매서드
DatagramSocket.java
public void receive(DatagramPacket p) throws IOException {
delegate().receive(p);
}
DatagramPacket.java
public DatagramPacket(byte[] buf, int offset, int length) {
setData(buf, offset, length);
}
DatagramPacket 클래스를 offset=0으로 생성하여 receive()로 감싸 계속 호출해주면 된다.
DatagramSocket은 소켓 통신을 하는 녀석
DatagramPacket은 바이트로 송수신할 메시지를 저장하고 반출하는 버퍼 타입 클래스 라고 생각해도 좋다.
수신된 데이터를 조회하기
DatagramPacket.java
public synchronized byte[] getData() {
return buf;
}
public synchronized int getLength() {
return length;
}
클라이언트에게 송신하기
DatagramSocket.java
public void send(DatagramPacket p) throws IOException {
delegate().send(p);
}
DatagramPacket.java
public DatagramPacket(byte[] buf, int offset, int length, SocketAddress address) {
setData(buf, offset, length);
setSocketAddress(address);
}
public synchronized SocketAddress getSocketAddress() {
return new InetSocketAddress(getAddress(), getPort());
}
클라이언트에게 보내기 위한 패킷은 받기위한 패킷과 달라야한다. 독립적이지 않으면 데이터 손실이 당연히 발생한다. 그리고 동시성문제도 발생하게 된다. 보낼일이 있다면 똑같은 어드레스 주소를 가진 녀석으로 인스턴스 하나 더 만들어줘야된다. recieve를 성공한 recieve전용 패킷에서 getSocketAddress를 이용하여 주소를 가져온뒤 생성자의 네번째 매개변수에 추가하여 send패킷을 만들어서 send 해주면 비로소 송신 수신을 둘다 구현하게 된다.
UDP Client
개요
이제 서버의 주소를 알고있는 클라이언트가 요청을 보내야한다.
요청을 보내기위한 준비 소켓 객체
DatagramSocket.java
public DatagramSocket() throws SocketException {
this(new InetSocketAddress(0));
}
요청을 보내기 위한 준비 패킷 객체
public DatagramPacket(byte[] buf, int length, SocketAddress address) {
this(buf, 0, length, address);
}
서버의 응답을 받기 위한 패킷 객체
public DatagramPacket(byte[] buf, int offset, int length) {
setData(buf, offset, length);
}
서버의 응답을 받는 소켓 매서드
public void receive(DatagramPacket p) throws IOException {
delegate().receive(p);
}
서버에 요청을 보내는 소켓 매서드
public void send(DatagramPacket p) throws IOException {
delegate().send(p);
}
JSON 데이터 형식
개요
JSON: Javascript Object Notation
파일명이다! 파일은 키-밸류 로 대응되는 형태로 여러 값들을 저장하고 있는 형태이다.
네트워크 통신에서는 규약이 있듯이 일관된 파일 형태로 주고 받아야 편리하다. 아무 브라우저에서 아무 사이트를 가도 다 데이터 주고 받을 수 있는건 약속된 JSON을 사용하기 때문이다.
만약 주고받는 파일의 규약을 통일 할 수 없었다면 에초에 망했을거다.
Java에서 데이터를 보내고 받아서 해석할때 HashMap key-value로 일일히 다 해석하게 하는 방법도 있겠지만 사실 이게 실체긴하지만, 라이브러리가 업데이트되고 발전하여 JSON-JAVA 외부 라이브러리를 이용하여 사용하면 버그없이 쉽게 사용할수 있도록하는 클래스들이 정의되어 있음.
클래스의 정체
public class JSONObject
오브젝트를 Json으로 출력할때 Json 오브젝트로 입력할때 그냥 객체 앵간하면 이걸 씀
public class JSONArray implements Iterable<Object>
오브젝트의 키-밸류가 아니라 다이렉트로 리스트 만들고 싶을때 이 녀석쓴다. 근데 애초에 JSONObject도 JSONArray도 서로가 서로를 참조하는 거진 강결합 형태나 마찬가지
JSONObject 생성자 및 매서드
빈오브젝트 만드는 법
public JSONObject() {
// HashMap is used on purpose to ensure that elements are unordered by
// the specification.
// JSON tends to be a portable transfer format to allows the container
// implementations to rearrange their items for a faster element
// retrieval based on associative access.
// Therefore, an implementation mustn't rely on the order of the item.
this.map = new HashMap<String, Object>();
}
파일디렉토리를 이용하여 JSONObject를 생성하는 법.
public JSONObject(String source) throws JSONException {
this(source, new JSONParserConfiguration());
}
JSONObject로부터 입력을 받는 개념중에 getxxx(String key)가 있다. xxx에 따라 받는게 다르다. 맞춰서 당연히 해줘야하고, 특히 Collection만 받으면 되면 그렇게 받지만, 프리미티브 타입은 전부 다 구현해줘야하기 때문에 JSONObject는 매서드 오버로딩이 많은 편
public Object get(String key) throws JSONException {
if (key == null) {
throw new JSONException("Null key.");
}
Object object = this.opt(key);
if (object == null) {
throw new JSONException("JSONObject[" + quote(key) + "] not found.");
}
return object;
}
put은 첫번째 매개변수는 항상 String key 고정이고, 두번째 매개변수의 타입을 다르게 지정함으로써 오버로딩이 구현되어 있다. 출력을 위한 대표적인 동작.
public JSONObject put(String key, Object value) throws JSONException {
if (key == null) {
throw new NullPointerException("Null key.");
}
if (value != null) {
testValidity(value);
this.map.put(key, value);
} else {
this.remove(key);
}
return this;
}
JSONObject 타입이거나 JSONArray타입을 추가할려고할때 이미 얘네 둘은 Object의 하위 객체이므로 this.map(key, value); 코드가 발동하고 저장하는데 무리가 없다. 나름 JSON 직렬화가 이미 진행된 객체이기 때문이다!!!
그런데 더 복잡한건 바로 나의 커스텀 객체를 출력하고자 할때 이다.
대체 뭐가 무엇을 믿고 객체만 넣으면 알아서 직렬화를 하냔 말이다. 키:값 의 형태로 반드시 직렬화를 해야한다….
추적의 결과 map.put(key, value)만 한다…!! 그냥 저장만 하는거야… 그럼 직렬화는 언제해????
JSONObject.toString()이 호출된 결과가 바로 직렬화의 결과다. 그 직렬화를 파일로 저장할 수도 그냥 사용할 수도 네트워크로 보낼 수도 있는거다. 그게 전부다.
@Override
public String toString() {
try {
return this.toString(0);
} catch (Exception e) {
return null;
}
}
@SuppressWarnings("resource")
public String toString(int indentFactor) throws JSONException {
// 6 characters are the minimum to serialise a key value pair e.g.: "k":1,
// and we don't want to oversize the initial capacity
int initialSize = map.size() * 6;
Writer w = new StringBuilderWriter(Math.max(initialSize, 16));
return this.write(w, indentFactor, 0).toString();
}
여기서 결국 핵심은 toString(0)이 무조건 호출되고, 그게 Writer를 생성하고 write()매서드를 호출
public Writer write(Writer writer) throws JSONException {
return this.write(writer, 0, 0);
}
@SuppressWarnings("resource")
public Writer write(Writer writer, int indentFactor, int indent)
throws JSONException {
try {
boolean needsComma = false;
final int length = this.length();
writer.write('{');
if (length == 1) {
final Entry<String,?> entry = this.entrySet().iterator().next();
final String key = entry.getKey();
writer.write(quote(key));
writer.write(':');
if (indentFactor > 0) {
writer.write(' ');
}
try{
writeValue(writer, entry.getValue(), indentFactor, indent);
} catch (Exception e) {
throw new JSONException("Unable to write JSONObject value for key: " + key, e);
}
} else if (length != 0) {
final int newIndent = indent + indentFactor;
for (final Entry<String,?> entry : this.entrySet()) {
if (needsComma) {
writer.write(',');
}
if (indentFactor > 0) {
writer.write('\n');
}
indent(writer, newIndent);
final String key = entry.getKey();
writer.write(quote(key));
writer.write(':');
if (indentFactor > 0) {
writer.write(' ');
}
try {
writeValue(writer, entry.getValue(), indentFactor, newIndent);
} catch (Exception e) {
throw new JSONException("Unable to write JSONObject value for key: " + key, e);
}
needsComma = true;
}
if (indentFactor > 0) {
writer.write('\n');
}
indent(writer, indent);
}
writer.write('}');
return writer;
} catch (IOException exception) {
throw new JSONException(exception);
}
}
여기서 이런 복잡한 코드가 숨겨져 있었네. 하는게 결국 여기서 직렬화를 하고 있다.
값에 대한 부분은 이런 코드가 동작하고 있는데..writeValue()...
@SuppressWarnings("resource")
static final Writer writeValue(Writer writer, Object value,
int indentFactor, int indent) throws JSONException, IOException {
if (value == null || value.equals(null)) {
writer.write("null");
} else if (value instanceof JSONString) {
// JSONString must be checked first, so it can overwrite behaviour of other types below
Object o;
try {
o = ((JSONString) value).toJSONString();
} catch (Exception e) {
throw new JSONException(e);
}
writer.write(o != null ? o.toString() : quote(value.toString()));
} else if (value instanceof String) {
// assuming most values are Strings, so testing it early
quote(value.toString(), writer);
return writer;
} else if (value instanceof Number) {
// not all Numbers may match actual JSON Numbers. i.e. fractions or Imaginary
final String numberAsString = numberToString((Number) value);
if(NUMBER_PATTERN.matcher(numberAsString).matches()) {
writer.write(numberAsString);
} else {
// The Number value is not a valid JSON number.
// Instead we will quote it as a string
quote(numberAsString, writer);
}
} else if (value instanceof Boolean) {
writer.write(value.toString());
} else if (value instanceof Enum<?>) {
writer.write(quote(((Enum<?>)value).name()));
} else if (value instanceof JSONObject) {
((JSONObject) value).write(writer, indentFactor, indent);
} else if (value instanceof JSONArray) {
((JSONArray) value).write(writer, indentFactor, indent);
} else if (value instanceof Map) {
Map<?, ?> map = (Map<?, ?>) value;
new JSONObject(map).write(writer, indentFactor, indent);
} else if (value instanceof Collection) {
Collection<?> coll = (Collection<?>) value;
new JSONArray(coll).write(writer, indentFactor, indent);
} else if (value.getClass().isArray()) {
new JSONArray(value).write(writer, indentFactor, indent);
} else {
quote(value.toString(), writer);
}
return writer;
}
와 라이브러리는 사용에 집중하자 이거 다 분석하려면 날 샌다…
병들고 약하여 이정도만 알아보는걸로 하고 넘어간다.
결론!! JSONObject와 JSONArray에는 각각 타입에 맞춰 구성하는 put(key, value) getXXX(key) toString()이 존재한다!! 직렬화의 시점은 toString()의 결과가 곧 직렬화의 결과다!!
TCP 채팅 프로그램
이것이 자바다를 참고하여 채팅 프로그램을 작성하였음... 나중에 정리해야겠다.
1. 서버앱이 실행할 Main코드
package network;
import java.util.*;
import java.net.*;
import java.io.*;
import java.util.concurrent.*;
import org.json.JSONObject;
//돌아갈 채팅 서버 클래스
public class ChatServer {
ServerSocket serverSocket; //연결을 받아주고 받으면 서버의 전용소켓을 생성하는 용
ExecutorService executorService = Executors.newFixedThreadPool(100);
//스레드 풀 전용 100개의 스레드가 돌아갈 수 있음 동시에
Map<String, SocketClient> chatRoom = Collections.synchronizedMap(new HashMap<>());
//챗룸은 연결하고 있는 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){
e.printStackTrace();
}
});
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());
}
public void removeSocketClient(SocketClient socketClient){
//소켓 클라이언트를 채팅방에서 삭제하는 매서드
String key = socketClient.chatName + "@" + socketClient.clientIp;
chatRoom.remove(key);
System.out.println("퇴장: " + key);
System.out.println("현재 채팅자 수: " + chatRoom.size());
}
public void sendToAll(SocketClient sender, String message){
//모든 소켓 클라이언트에게 메시지를 보내는 매서드
JSONObject jsonObject = new JSONObject();
jsonObject.put("clientIp", sender.clientIp);
jsonObject.put("chatName", sender.chatName);
jsonObject.put("message", message);
String json = jsonObject.toString(); //직렬화
Collection<SocketClient> socketClients = chatRoom.values();
for(SocketClient sc : socketClients){ //브로드 캐스팅
if(sc == sender) continue;
sc.send(json);
}
}
public void stop(){
try{
serverSocket.close();
executorService.shutdown();
chatRoom.values().stream().forEach(sc -> sc.close());
}catch(IOException e){
e.printStackTrace();
}
}
public static void main(String[] args) {
try{
ChatServer chatServer = new ChatServer();
chatServer.start();
System.out.println("q를 누르면 채팅 서버를 종료합니다.");
Scanner scanner = new Scanner(System.in);
while(true){
String key = scanner.nextLine();
if(key.equals("q")){
break;
}
}
chatServer.stop();
scanner.close();
}catch(IOException e){
e.printStackTrace();
}
}
}
2. 서버앱에서 호출하여 소켓을 관리하기 위한 클래스
package network;
import java.net.*;
import java.io.*;
import org.json.*;
public class SocketClient {//서버가 다리놔주세요 신호 받으면 생성할 객체임.
Socket socket; //자바 소켓 객체 주고받는건 이녀석 담당.
DataInputStream dataInputStream; //어떻게 해석할지
DataOutputStream dataOutputStream; //뭘 보낼지
ChatServer chatServer; //연결이 성공하면 담을 서버객체
String clientIp; //내 IP 포트 들어갈 정
String chatName; //내가 만든 채팅 이름
public SocketClient(ChatServer chatServer, Socket socket){
try{
//연결시도가 성공하면 비로소 호출할 것. 애초에 ChatServer에서 호출하는 놈임
this.chatServer = chatServer;
this.socket = socket;
this.dataInputStream = new DataInputStream(socket.getInputStream());
this.dataOutputStream = new DataOutputStream(socket.getOutputStream());
InetSocketAddress isa = (InetSocketAddress)socket.getRemoteSocketAddress();
//이게 요청에 의해 만들어진 객체이다. 즉 소켓 클라이언트 객체가 만들어진 것이다.
//따지면 요청이 있을때 반응하는 객체니까 클라이언트 객체란 이름이 틀리진 않다.
//약각 request객체 느낌??
this.clientIp = isa.getHostName();
receive();
}catch(IOException e){
e.printStackTrace();
}
}
public void receive(){
chatServer.executorService.execute(() -> {
try{
while(true){
String receiveJson = dataInputStream.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{
dataOutputStream.writeUTF(json);
dataOutputStream.flush();
}catch(IOException e){
e.printStackTrace();
}
}
public void close(){
try{
socket.close();
}catch(IOException e){
e.printStackTrace();
}
}
}
3. 클라이언트 역할을 할 앱의 main
package network;
import 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 (Exception 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.println("대화명 입력: ");
chatClient.chatName = scanner.nextLine();
JSONObject jsonObj = new JSONObject();
jsonObj.put("command", "incoming");
jsonObj.put("data", chatClient.chatName);
String json = jsonObj.toString();
chatClient.send(json);
chatClient.receive();
System.out.println("=========");
System.out.println("보낼 메시지 입력하고 Enter");
System.out.println("클라이언트 종료하려면 q enter");
System.out.println("=========");
while (true) {
String message = scanner.nextLine();
if(message.toLowerCase().equals("q")){
break;
} else {
jsonObj = new JSONObject();
jsonObj.put("command", "message");
jsonObj.put("data", message);
json = jsonObj.toString();
chatClient.send(json);
}
}
scanner.close();
chatClient.unconnect();
} catch (Exception e) {
System.err.println(e);
}
}
}
'Learn > 이것이 자바다' 카테고리의 다른 글
[이것이 자바다] [확인문제] 네트워크 입출력 (0) | 2024.12.17 |
---|---|
[이것이 자바다 확인 문제] chapter 17 (0) | 2024.12.12 |
Stream Interface (1) | 2024.12.12 |
[이것이 자바다 확인문제] chapter 16 (0) | 2024.12.11 |
람다식, 함수형 프로그래밍, 메소드 참조, 생성자 참조 (1) | 2024.12.11 |