오늘은 1:N 채팅 만들기 시간이었다.
import java.io.*; // io 호출
import java.net.*; // net 호출
class Client extends Thread { // 클라이언트도 I/O를 받아들여야 하기 때문에 쓰레드 여야함! 따라서, Thread 상속선언해줌
BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); // buffered reader를 노드,브릿지,필터를 한 줄로 통합하여 사용한 선언
Socket s; // 소켓 멤버변수 선언
InputStream is; // 인풋 스트림 멤버변수 선언
OutputStream os; // 아웃풋 스트림 멤버변수 선언
DataInputStream dis; // 데이터 인풋(필터스트림) 멤버변수 선언
DataOutputStream dos; // 데이터 아웃풋(필터스트림) 멤버변수 선언
String chatId; // 채팅 아이디 멤버변수 선언
Client(){
connect(); // 서버와 접속 선언
}
void connect(){ // connect 메소드 선언
try{ // connect 메소드의 예외 발생 구문
p("서버IP(기본:127.0.0.1): "); // 서버IP(기본:127.0.0.1): 를 모니터에 출력
String ip = br.readLine(); // 사용자에게 입력받는 구문
if(ip != null) ip = ip.trim(); // ip가 null이 아니면 공백 제거
if(ip.length() == 0) ip = "127.0.0.1"; // 입력받은 게 없으면 ip는 기본 로컬호스트로 입력
p("PORT(기본:3000): "); // PORT(기본:3000): 를 모니터에 출력
String portStr = br.readLine(); // 사용자에게 입력받는 구문
if(portStr != null) portStr = portStr.trim(); // port 스트링이 null이 아니면 공백 제거
if(portStr.length() == 0) portStr = "3000"; // 입력받은 게 없으면 port는 기본 포트로 입력
int port = Integer.parseInt(portStr); // 포트 번호를 정수형으로 반환
if(port<0 || port>65535){ // 포트번호가 0 ~ 65535 를 벗어난 경우
pln("범위가 유효하지 않은 포트임"); // 범위가 유효하지 않은 포트임이라고 모니터에 출력
connect(); // 그리고 다시 connect 메소드 호출하여 연결시도
return; // 그리고 이 부분은 다시 실행 못하게 나가버림
}
s = new Socket(ip, port); // 입력한 ip와 port를 담은 새로운 소켓을 만들어줌
pln("서버와 연결 성공"); // 만든 후 "서버와 연결 성공"을 모니터에 출력
is = s.getInputStream(); // 서버가 보냈다면 소켓에 있는 값을 인풋스트림으로 가져와서 is에 넣음
os = s.getOutputStream(); // os에 담긴 값을 소켓에 보낸다.
dis = new DataInputStream(is); // is값을 DataInputStream형으로 바꿔줌
dos = new DataOutputStream(os); // os값을 DataOutputStream형으로 바꿔줌
start(); // 여기서 run 메소드 불러와서 동시에 스레드로 돌려주기 시작함
// run 메소드는 메세지가 오면 모니터에 띄워주는 역할을 함
// 스레드로 돌아가야 언제든 메세지가 와도 받을 수 있음
inputChatId(); // 채팅 아이디 메소드 호출
}catch(IOException ie){
}
}
public void run(){ // listen (socket -> monitor) 해주는 메소드 쓰레드로 돌림
try{ // 쓰레드 실행 예외발생구문
while(true){ // 언제 메세지가 올 지 모르니까 무한루프로 계속 돌려줌
String msg = dis.readUTF(); // 만약에, 소켓에서 메세지가 들어오면 msg라는 string 타입 변수에 UTF로 변환한 메세지를 담음
pln(msg); // 위에서 받은 msg 내용 모니터에 출력
}
}catch(IOException ie){ // 입출력 예외 발생시 처리해주는 구문 시작
pln("서버가 다운됨.. 2초 후에 종료됩니다."); // 서버가 다운됨.. 2초 후에 종료됩니다. 모니터에 출력
try{ // 예외 발생 구문
Thread.sleep(2000); // 2초 후에 종료하게끔 쓰레드 주어진 시간만큼 일시정지
System.exit(0); // 정상 종료
}catch(InterruptedException iie){} // ie라고 적어주면 지역변수 이름이 충돌하니까 iie로 변경
}finally{
closeAll(); // 마침내 모든 걸 닫아주는 메소드 소환
}
}
void inputChatId(){ // 채팅 ID를 입력받는 메소드
p("채팅ID(기본:GUEST) : "); // 채팅ID(기본:GUEST) : 를 모니터에 출력
try{ // inputChatId의 메소드 예외 발생 구문
chatId = br.readLine(); // chatId의 사용자 입력값을 읽어줌
if(chatId != null) chatId = chatId.trim(); // chatId가 null이 아니면 chatId의 공백 제거
if(chatId.length() == 0) chatId = "GUEST"; // 입력받은 게 없으면 chatId는 GUEST로 입장
dos.writeUTF(chatId); // 소켓에 출력하는 부분인데 출력스트림을 dos에 넣는 거야
// dos에 문자열인 chatId를 파일에 쓰기위한 함수 호출 == writeUTF
dos.flush(); // 현재 버퍼에 저장되어 있는 내용을 클라이언트로 전송하고 버퍼를 비운다.
inputMsg(); // inputMsg 메소드를 호출한다.
}catch(IOException ie){ // 입출력 예외 발생시 처리해주는 구문 시작
}
}
void inputMsg(){ // Speak (key -> socket) 해주는 메소드
String msg = ""; // msg 초기값 선언
try{ // inputMsg의 예외 발생 구문
while(true){ // 내가 입력값 언제 입력할 지 모르니까 대기가 필요하므로 무한루프 시작
msg = br.readLine(); // 내가 키보드에서 입력받은 값을 msg를 읽어줌
dos.writeUTF(chatId + ">> " + msg); // 발송하긴 할건데, dos에 문자열인 chatId의 msg를 읽어줌
dos.flush(); // 현재 버퍼에 저장되어 있는 내용을 클라이언트로 전송하고 버퍼를 비운다.
}
}catch(IOException ie){ // 입출력 예외 발생
}finally{
closeAll(); // 무한루프가 끝났다? 라는 의미, 모든 것을 닫아줌
}
}
void closeAll(){ // 연결 객체들 닫기
try{ // closeAll의 메소드 예외 발생 구문
// 그냥 두면 메모리 낭비되니까, 가비지 컬렉터가 가져가라고 보내줌
if(dis != null) dis.close(); // filter부터 닫아줘야 하므로, dis 닫아주기
if(dos != null) dos.close(); // dos 닫아주기
if(is != null) is.close(); // is 닫아주기
if(os != null) os.close(); // os 닫아주기
if(s != null) s.close(); // 소켓 닫아주기
}catch(IOException ie){}
}
void pln(String str){ // 프린트 해주는 함수
System.out.println(str);
}
void p(String str){ // 프린트 해주는 함수
System.out.print(str);
}
public static void main(String args[]){
new Client(); // client 생성자 호출
}
}
import java.io.*; // io를 불러들임
import java.net.*; // net을 불러들임
import java.util.*; // util을 불러들임
class Server {
ServerSocket ss; // 서버니까 서버소켓을 ss로 잡고,
Socket s; // 연결되면 소켓이 생성되니까 소켓을 s로 잡는다
int port = 3000; // port도 정수형태로 받는다.
Vector<OneClientModule> v = new Vector<OneClientModule>();
// oneclientmodule을 vector로 만들어주기, 왜?
// 클라이언트가 접속할때 소켓을 만들면서 모듈로 묶어줌
// 앞으로 생기는 클라이언트와의 소통을 모듈안에서 하게 함
OneClientModule ocm;
// 모듈을 안에서 불러야 하니까 객체 만들어줌
// oneclientmodule 적어주면 너무 기니까 ocm으로 줄여버리기
Server(){ // 서버를 생성자로 선언하고
try{ // 서버 생성자의 예외 발생 구문
ss = new ServerSocket(port); // 서버 소켓에 포트번호를 받아 선언해준다.
pln(port+ "번 포트에서 서버 대기중..."); // ~번 포트에서 대기중이라고 출력되게끔 해준다.
while(true){ // 무한루프 시작
// 무한루프 이유 - 계속 돌아야 언젠가 접속이 들어오면 바로 생성할 수 있게 하기 위함
s = ss.accept(); // 접속 들어오면 소켓 s 생성, 하나의 클라이언트와 통신하는 서버 모듈클래스
ocm = new OneClientModule(this);
// 위의 경우는, (s)로 할 경우 읽을 순 있는데, 모든 사람한테 v를 뿌려줄 때 문제가 됨
// 따라서 (v, s)혹은 (this)를 넘겨줘야 함
v.add(ocm);
// 이 모듈이 여러개 만들어질 예정이므로, 언제든 갯수가 추가되거나 삭제가 가능한 가변배열에 넣어줌
// v안에는 연결된 클라이언트 모듈들이 들어갈 예정 그래야 각 모듈별로 io를 주고받을 수 있음
ocm.start();
// 그리고 쓰레드로 해주지 않으면 한 모듈에 붙잡혀서 동시 진행이 안 됨
// 그래서 미리 그 모듈이 스레드로 돌아가는 모듈로 만들어놓고 start 해서 동시에 스레드로 돌아가게 해줌
}
}catch(IOException ie){ // IOException 예외처리, 여기서 발생하는 예외는 api보니까 서버와 연결이 안 되는 것
// 이유는 포트가 겹치는 경우이므로, 포트가 겹칠경우 밑의 멘트를 출력해줌
pln(port+"번 포트는 이미 사용중임");
}finally{ // 위의 걸로도 못잡으면 마지막 유언을 남기는 데 그게 뭐냐면
try{
if(ss != null) ss.close(); // 서버소켓이 null이 아닌 경우 닫아주기
// @@@@@@ 더 이상 서버소켓이 사용되지 않을때 == 연결 안할 때 만들었던 서버소켓을 메모리 줄이게 반환시킴
}catch(IOException ie){} // IOException 예외처리
}
}
void pln(String str){
System.out.println(str);
}
void p(String str){
System.out.print(str);
}
public static void main(String[] args){
new Server();
}
}
import java.io.*;
import java.net.*;
// 하나의 클라이언트와 통신(특정 클라이언트에서 들어서 나머지 클라이언트들에게 뿌려줌)하는 서버 모듈 클래스
class OneClientModule extends Thread {
//각 클라이언트마다 모듈을 하나씩 가지게 됨.
//이 모듈을 통해서 서버와 클라이언트가 소통을 원활히 할 수 있게 함!
//동시수행 되어야 하므로 스레드 상속 받아줌.
Server server;
// 먼저 내가 만든 서버를 다른 클래스에서 가져와야 하니까 서버 불러줌
Socket s;
InputStream is;
OutputStream os;
DataInputStream dis;
DataOutputStream dos;
String chatId = "GUEST";
OneClientModule(Server server){
// 서버랑 연결해서 사용할거니까, 매개변수로 서버를 가져옴
this.server = server; // 이거는 서버
this.s = server.s; // 이거는 소켓
try{
is = s.getInputStream(); // 소켓에서 인풋값 받아서 is에 저장
os = s.getOutputStream(); // os에 있는거 아웃풋 시켜줌
dis = new DataInputStream(is); // is에 있는거 datainputstream으로 바꿔줌
dos = new DataOutputStream(os); // os에 있는거 dataoutputstream으로 바꿔줌
}catch(IOException ie){}
}
public void run(){ // listen -> broadcasting
listen(); // start를 선언하면, 리슨메소드를 스레드로 돌려줌
}
void listen(){ // listen 메소드 호출
String msg = ""; // msg 메세지에 빈값으로 초기화 시켜줌
try{ // listen 메소드에서 예외 발생 구문
chatId = dis.readUTF(); // 소켓에서 chatId를 UTF파일로 읽어준 값으로 아이디를 설정해줌
String inMsg = chatId + "님 입장!! (총인원:" + server.v.size()+ "명)"; // 누가 들어오면 뜰 메세지를 inMsg에 넣어줌
broadcast(inMsg); // 미리 설정해둔 inMsg를 broadcast 시킴
server.pln(inMsg); // 서버에도 inMsg를 broadcast 시킴
while(true){ // 무한루프 시작, 계속 실행하게끔 해주려고 무한루프 시작
msg = dis.readUTF(); // 소켓에 메세지가 도착하면 (누군가 보내거나 서버가 보내면) 그 내용을 is>dis>msg에 담아서
broadcast(msg); // broadcast 해준다
server.pln(msg); // 서버도 그 메세지를 받아서 똑같이 모니터에 출력 해준다
}
}catch(IOException ie){
server.v.remove(this); // 누군가 사라지면(끄면), 이 모듈 자체가 그 사람의 모듈이니까 이 모듈을 삭제시킴
String outMsg = chatId+"님 퇴장!!(총인원: " + server.v.size() + "명)"; // 퇴장시 채팅 아이디와 서버의 인원을 outMsg에 담아줌
broadcast(outMsg); // outMsg를 담아 broadcast 해줌
server.pln(outMsg); // 서버도 그 메세지를 받아서 똑같이 모니터에 출력 해준다
}finally{
closeAll();
}
}
void closeAll(){ // 연결객체들 닫기
try{
if(dis != null) dis.close();
if(dos != null) dos.close();
if(is != null) is.close();
if(os != null) os.close();
if(s != null) s.close();
}catch(IOException ie){}
}
void broadcast(String msg){ // broadcast 하는 메소드, msg 스트링을 받아와서 모두에게 뿌려주는 메소드
try{
for(OneClientModule ocm: server.v){ // 벡터 v안에 있는 모든 모듈들(즉 누군가 접속했기때문에 생긴모듈들에게)
ocm.dos.writeUTF(msg); // 내가 쓴 메세지를 output시킴
ocm.dos.flush(); //끝까지 밀어넣어서 보내줌
}
}catch(IOException ie){}
}
}
ServerSocket
은 java에서 서버 프로그램을 개발할 때 쓰이는 클래스고, Socket
클래스는 client에서 서버로 접속하거나 Server에서 accept 하는데 필요한 클래스다.