java.net package + 채팅 기능 만들기
네트워킹은 두 대 이상의 컴퓨터들을 서로 연결하여 네트워크를 구성하는 것을 의미한다.
이러한 개념은 서로 다른 컴퓨터 간에 데이터를 손쉽게 주고받을 수 있게 하기 위해 시작되었다!
자바의 java.net 패키지를 사용하면 네트워크 어플리케이션의 데이터 통신 부분을 작성할 수 있다.

클라이언트(client) / 서버(server)
클라이언트와 서버라는 단어는 컴퓨터 간의 관계를 "역할"로 구분한다.
서버(server)는 서비스를 제공하는 컴퓨터이고,
클라이언트(client)는 그 서비스를 사용하는 컴퓨터를 의미한다.
서비스는 서버가 클라이언트로부터 요청받은 작업을 처리해서, 그 결과를 제공하는 것을 의미한다.
이 때 서비스는 종류에 따라 파일서버, 메일서버, 어플리케이션 서버 등이 있다.
그리고 서버와 서비스의 수가 꼭 일치하지는 않는다.
제공하는 서비스는 하나인데 서버가 여러 대일수도 있고, 제공하는 서비스가 여러개인데 서버는 한 대일수도 있다.
서버가 서비스를 제공하기 위해서는 서버 프로그램이 필요하고,
클라이언트가 서비스를 제공받기 위해서는 서버 프로그램과 연결할 수 있는 클라이언트 프로그램이 필요하다.
서버를 짤 때는 항상 두 개를 짠다.
1. 접속을 담당하는 클래스 : 1개만 수행하면 됨
2. 통신을 담당하는 클래스 : 클라이언트 개수(접속자수)만큼 프로그램이 실행돼야 함
소켓 프로그래밍

소켓(Socket)이란 프로세스 간의 통신에 사용되는 양쪽 끝단을 의미한다.
TCP/IP 네트워크를 이용해 쉽게 통신 프로그램을 작성하도록 지원하는 기반 기술이다.
ServerSocket 클래스와 Socket클래스 등을 이용해서 구현할 수 있다.
ServerSocket 클래스
전화기로 예를 들자면...
서버소켓(구분이 잘 안 돼서 이하 ServerSocket)은 전화교환기, 개인 전화기는 소켓이라고 할 수 있다.
전화기를 사용하려면 우선 개통이 필요하다.
개통을 하려면 전화번호(IP)와 전화선(PORT) 연결이 필요하다.
IP는 네트워크상의 컴퓨터나 시스템을 식별하는 주소이고,
PORT는 통신할 응용 프로그램을 식별하는 데에 쓰인다.
(이 때 포트넘버는 0번부터 65535번까지 있으며 응용 프로그램별로 주로 쓰이는 번호가 정해져 있다.)
| 포트 넘버 | 응용 프로그램 |
| 80 | HTTP |
| 23 | TELNET |
| 21 | FTP(파일 전송 프로토콜) |
| 25 | SMTP |
| 1521 | Oracle |
| 1433 | MS-SQL |
| 3306 | MY-SQL |
| 8080 | 프록시 서버 |
| 4000 | 머드 서버 |
개통이 끝났다면 전화가 잘 오는지 한 번 들어봐야 한다. = listen() 메서드
전화교환기는 listen() 메서드로 전화기의 연결 요청을 기다리고 있어야 한다.
클라이언트가 연결을 요청했을 때 accept()로 받아 주면서, 클라이언트의 발신자 정보를 얻어 온다.
발신자가 들어 오면 발신자 정보(IP, PORT)를 메모리에 저장한다.
그리고 클라이언트의 정보를 스레드로 넘겨 주고,
클라이언트와 스레드가 각각 통신이 가능하도록 만들어 준다!

다시 말해...
서버 프로그램에서 ServerSocket으로 서버의 특정 포트에서 클라이언트의 연결 요청을 처리할 준비를 한다.
→ 클라이언트 프로그램은 접속할 서버의 IP 주소와 포트 정보를 가지고 소켓을 생성해 서버에 연결을 요청한다.
→ ServerSocket이 클라이언트의 연결 요청을 받으면 서버에 새로운 소켓을 생성, 클라이언트의 소켓과 연결되도록 한다.
→ 이제 클라이언트의 소켓과 새로 생성된 서버의 소켓은 ServerSocket과 관계없이 1:1 통신을 한다.
ServerSocket은 포트와 결합(bind)되어 포트를 통해 원격 사용자의 연결 요청을 기다리다가,
연결 요청이 올 때마다 새로운 소켓을 생성하여 상대편 소켓과 통신할 수 있도록 연결한다.
소켓과 소켓이 통신할 수 있도록 연결(까지만)하는 것이 서버소켓의 역할이고,
우선 연결되고 나면 실제 데이터는 소켓끼리 주고 받는다.

Socket 클래스
소켓은 프로세스 간의 통신을 담당하며, 입력 스트림과 출력스트림을 가지고 있다.
그리고 이 스트림들은 연결된 상대편 소켓의 스트림들과 교차 연결되어 프로세스간의 통신이 이루어진다.
A 소켓의 입력 스트림은 B 소켓의 출력 스트림에,
A 소켓의 출력 스트림은 B 소켓의 입력 스트림에 연결된다.
| 생성자 | 설명 |
| Socket(InetAddress address, int port) | 소켓을 생성하고, 지정된 IP 주소와 포트 번호에서 대기하는 원격 응용프로그램의 소켓에 연결한다. |
| Socket | 연결되지 않은 상태의 소켓을 생성한다. |
Thread
데이터를 송신하는 작업과 수신하는 작업을 동시에 처리하고 싶을 때는? (실시간 채팅)
→ 스레드가 프로그램 위에서 여러 개의 프로그램을 동시에 수행하게 해 준다!
하나의 서버 프로세스가 여러 개의 스레드를 생성해서 스레드와 사용자의 요청이 일대일로 처리되도록 한다.
모든 프로그램은 1개 이상의 스레드를 가지고 있다.
스레드를 사용하는 방법에는 두 가지 방법이 있다.
1. Runnable 인터페이스 구현
class MyThread extends Thread {
public void run(){/*작업내용*/} //Thread클래스의 run() 오버라이딩
}
스레드 생성 방법
Runnable 인터페이스를 구현한 클래스의 인스턴스를 생성하고,
그 인스턴스를 Thread 클래스의 생성자의 매개변수로 제공해야 한다!
2. Thread 상속
class MyThread implements Runnable{
public void run(){/*작업내용*/} //Runnable 인터페이스의 run() 구현
}
얘는 그냥 보통 클래스로 생성하듯이 생성하면 됨. (ThreadChild tc = new ThreadChild() 이렇게)
new Thread() 를 만들어준다고 실행이 바로 되는 것은 아니다.
→ start()를 호출해 주어야 한다! *start()는 한 번만 호출가능하다. 실행이 종료된 스레드는 다시 실행할 수 없음.
→ start()를 호출해 주면 우선 대기상태로 들어갔다가 자신의 차례가 되면 실행이 된다.
start()는 새로운 스레드가 작업을 수행하는 데 필요한 호출스택을 생성한 다음 run()을 호출해서,
생성된 호출 스택에 run()이 첫 번째로 올라가도록 한다!
그러니까... 우리가 구현하는 건 run()이지만 start()만 호출하면 된다.
이 모든것을 응용해서 채팅 프로그램 작성...
doodoo.server.Server.java
package doodoo.server;
import java.util.*;
import doodoo.common.Function;
import java.net.*;
import java.io.*;
public class Server implements Runnable { //접속만 담당하는 클래스
private Vector < Client > waitVc = new Vector < Client > ();
private ServerSocket ss;
private final int PORT = 3355;
public Server() {
try {
ss = new ServerSocket(PORT);
// bind(), listen() 이라는 메서드가 핵심임.
// 생성자 안에서 처리하면 생성하자마자 연결되고 대기상태까지 이어짐.
// 근데 단점이 있다. 50명 이상이 들어가면 서버가 무너진다...(디폴트 50)
// ss = new ServerSocket(PORT,1000) 이런식으로 인원 지정도 가능함.
System.out.println("Server Start!");
} catch (Exception ex) {}
}
@Override
public void run() { //접속을 어떻게 처리할 것인가
try {
while (true) {//클라이언트의 요청을 지속적으로 처리하기 위해 무한반복
Socket s = ss.accept(); //특별한 메서드. 클라이언트가 접속이 됐을 때만 필요하다.
//평상시에는 멈춰 있다가 클라이언트가 접속이 되면 호출이 된다.
//암튼 "통신을 시작해라" 는 의미.
Client client = new Client(s);
client.start(); //run() -> 통신을 시작한다.
}
} catch (Exception ex) {}
}
public static void main(String[] args) {
Server server = new Server();
new Thread(server).start(); //접속을 받는다
}
//통신한 내용이 공유가 되어야 한다. 이 때 필요한 것? 내부 클래스!
class Client extends Thread { //통신하는 클래스
private String id, name, sex, pos;
private Socket s; //클라이언트의 정보를 갖고 있다(ip, port)
private BufferedReader in ; //클라이언트의 요청값을 받는다
private OutputStream out; //클라이언트로 결과값을 보내준다
public Client(Socket s) {
try {
this.s = s; in = new BufferedReader(new InputStreamReader(s.getInputStream()));
//InputSreamReader : 필터 스트림(1byte를 2byte로 변환) -> 한글 깨짐 방지
//네트워크 전송은 byte단위로 전송해야 함.
//받는 경우에는 문자 스트림으로 읽어 와야함. 그래야 한글이 안깨진다!
out = s.getOutputStream();
//s는 클라이언트의 전화기(=소켓)
//서버는 클라이언트의 정보를 알아야 하고, 클라이언트는 서버의 정보를 알아야 한다.
//서버 == 클라이언트의 정보(IP/PORT(Socket), 읽어올 메모리(in), 읽어갈 메모리(out))
//클라이언트 == 서버의 정보(IP/PORT(Socket), 읽어올 메모리(in), ...
//in => BufferedReader, out => OutputStream
//클라이언트마다 따로 작동해야할 때 스레드를 상속받는다.
} catch (Exception ex) {}
} // 여기까지는 웹 서버가 이미 만들어준다.
//여기부터 우리가 만들어나가야 하는 부분
//이제 통신 기능을 만들어보자
//클라이언트가 요청하면 어떻게 처리해줄 것인가?
public void run() {
try {
//100|id|name|sex 이런 형식의 데이터를 받아올 것이다
while (true) {
//클라이언트의 요청값을 받아오자
String msg = in .readLine();
StringTokenizer st = new StringTokenizer(msg, "|");
int protocol = Integer.parseInt(st.nextToken());
switch (protocol) {
case Function.LOGIN: {
//데이터를 받고,
id = st.nextToken();
name = st.nextToken();
sex = st.nextToken();
pos = "대기실";
//이미 로그인된 사람에게 정보를 보내 주고,
messageAll(Function.LOGIN + "|" +
id + "|" + name + "|" + sex + "|" + pos);
messageAll(Function.WAITCHAT + "|알림>>" + name + "입장하셨습니다");
//waitVc에 저장하고,
waitVc.add(this);
messageTo(Function.MYLOG + "|" + id);
//로그인한 사람에게 접속한 사람 정보를 전송한다.
for (Client client: waitVc) {
messageTo(Function.LOGIN + "|" +
client.id + "|" + client.name + "|" +
client.sex + "|" + client.pos);
}
//개설된 방 정보도 보내준다
}
break;
case Function.WAITCHAT: {
String data = st.nextToken();
messageAll(Function.WAITCHAT + "|[" + name + "]" + data);
}
break;
}
}
} catch (Exception ex) {}
}
//서버에서 응답하는 방법은 두 가지가 있다.
//1. 한 명에게만 보내는 것
//synchronized -> 동기화
public synchronized void messageTo(String msg) {
try {
out.write((msg + "\n").getBytes());
//문자열을 바이트로 보내는 것 = 인코딩 <-> 받을 때는 디코딩
} catch (Exception ex) {}
}
//2. 접속한 모든 사람에게 보내는 것
public synchronized void messageAll(String msg) {
try {
for (Client client: waitVc) {
client.messageTo(msg);
}
} catch (Exception ex) {}
}
}
}
doodoo.common.Function.java
package doodoo.common;
//내부 프로토콜 : client/server
//client : 요청 ---------------------> request
//server : 요청 처리-->결과값 전송(응답)---> response
//웹->네트워크(TCP)->연결 지향적 프로그램
//오라클(서버)->응용프로그램(DAO, 클라이언트)
public class Function {
public static final int LOGIN = 100;
public static final int MYLOG = 110;
public static final int MAKEROOM = 200;
public static final int ROOMIN = 210;
public static final int MYROOM = 220;
public static final int ROOMOUT = 230;
public static final int MYROOMOUT = 240;
public static final int WAITUPDATE = 250;
public static final int WAITCHAT = 300;
public static final int ROOMCHAT = 310;
public static final int CHATEND = 900;
public static final int MYCHATEND = 910;
}
doodoo.client.Login.java
package doodoo.client;
import javax.swing.*;
import java.awt.*;
public class Login extends JPanel {
private Image back;
JLabel la1, la2, la3;
JTextField tf1, tf2;
JRadioButton rb1, rb2;
JButton b1, b2;
public Login() {
back = Toolkit.getDefaultToolkit().getImage("c:\\javaDev\\back.jpg");
setLayout(null);
//메모리할당
la1 = new JLabel("ID");
la2 = new JLabel("NAME");
la3 = new JLabel("SEX");
tf1 = new JTextField();
tf2 = new JTextField();
rb1 = new JRadioButton("M");
rb2 = new JRadioButton("F");
rb1.setSelected(true);
ButtonGroup bg = new ButtonGroup();
bg.add(rb1);
bg.add(rb2); //한번에 하나씩만 선택가능하도록
b1 = new JButton("LOGIN");
b2 = new JButton("CANCEL");
//배치
la1.setBounds(10, 15, 50, 30);
tf1.setBounds(65, 15, 150, 30);
la2.setBounds(10, 50, 50, 30);
tf2.setBounds(65, 50, 150, 30);
la3.setBounds(10, 85, 50, 30);
rb1.setBounds(65, 85, 70, 30);
rb2.setBounds(140, 85, 70, 30);
JPanel p = new JPanel();
p.add(b1);
p.add(b2);
p.setBounds(10, 125, 210, 30);
add(la1);
add(tf1);
add(la2);
add(tf2);
add(la3);
add(rb1);
add(rb2);
add(p);
}
@Override
protected void paintComponent(Graphics g) {
g.drawImage(back, 0, 0, getWidth(), getHeight(), this);
}
}
doodoo.client.WaitRoom.java
package doodoo.client;
import java.awt.*;
import javax.swing.*;
import javax.swing.table.DefaultTableModel;
public class WaitRoom extends JPanel {
JTable table1, table2;
DefaultTableModel model1, model2;
JTextArea ta;
JTextField tf;
JButton b1, b2, b3, b4, b5, b6;
public WaitRoom() {
ta = new JTextArea();
JScrollPane js1 = new JScrollPane(ta);
ta.setEditable(false);
tf = new JTextField();
b1 = new JButton("방 만들기");
b2 = new JButton("방 들어가기");
b3 = new JButton("쪽지 보내기");
b4 = new JButton("정보 보기");
b5 = new JButton("뮤직");
b6 = new JButton("나가기");
String[] col1 = {
"방이름",
"상태",
"인원"
};
String[][] row1 = new String[0][3];
model1 = new DefaultTableModel(row1, col1);
table1 = new JTable(model1);
JScrollPane js2 = new JScrollPane(table1);
String[] col2 = {
"ID",
"이름",
"성별",
"위치"
};
String[][] row2 = new String[0][3];
model2 = new DefaultTableModel(row2, col2) {
// 내부 클래스는 쓰레드->멤버클래스
// 공통으로 사용되는 변수, 메서드가 있을 때. (스레드랑 클라이언트같은거)
// 익명 클래스->상속 없이 오버라이딩시킬 때 쓴다.
@Override
public boolean isCellEditable(int row, int column) {
return false;
}
};
table2 = new JTable(model2);
JScrollPane js3 = new JScrollPane(table2);
//배치
setLayout(null); //직접 배치를 하겠다는 뜻
js2.setBounds(10, 15, 450, 250);
add(js2);
js3.setBounds(10, 275, 450, 340);
add(js3);
js1.setBounds(470, 15, 300, 350);
add(js1);
tf.setBounds(470, 375, 300, 30);
add(tf);
JPanel p = new JPanel();
p.add(b1);
p.add(b2);
p.add(b3);
p.add(b4);
p.add(b5);
p.add(b6);
p.setLayout(new GridLayout(3, 2, 5, 5));
p.setBounds(470, 415, 300, 190);
add(p);
}
}
doodoo.client.ClientMain.java
package doodoo.client;
//window
import javax.swing.*;
import doodoo.common.Function;
import java.awt.*;
import java.awt.event.*;
//network
import java.util.*;
import java.net.*;
import java.io.*;
public class ClientMain extends JFrame implements ActionListener, Runnable {
CardLayout card = new CardLayout();
Login login = new Login();
WaitRoom wr = new WaitRoom();
//Network 관련
Socket s; //서버와 연결된 연결기기
BufferedReader in ; //서버로부터 값을 읽어오는 부분
OutputStream out; //서버로 값을 보낸다
public ClientMain() {
setLayout(card); //배치
add("LOGIN", login);
add("WR", wr);
setSize(800, 700); //윈도우 크기
setVisible(true); //윈도우 보여달라
setDefaultCloseOperation(EXIT_ON_CLOSE); //종료
//이벤트 등록
login.b1.addActionListener(this); //로그인
login.b2.addActionListener(this); //로그인 취소
wr.tf.addActionListener(this);
wr.b6.addActionListener(this); //나가기
}
//서버 연결
public void connection(String id, String name, String sex) {
try {
s = new Socket("localhost", 3355); //서버 연결해서 서버 정보를 읽어옴(ip, port)
in = new BufferedReader(new InputStreamReader(s.getInputStream()));
//서버에 있는 값을 읽어 오겠다
out = s.getOutputStream(); //서버 쪽으로 값을 보내라
//로그인 요청
out.write((Function.LOGIN + "|"
+ id + "|" + name + "|" + sex + "\n").getBytes());
} catch (Exception ex) {}
//try-catch 가 끝나고 스레드를 이용해서 서버로부터 값을 읽어라.
new Thread(this).start(); //run 메서드 호출
}
//서버에서 값을 읽어 와서 출력하는 위치
@Override
public void run() {
try {
while (true) {
String msg = in .readLine(); //서버 응답값 받기
StringTokenizer st = new StringTokenizer(msg, "|");
int protocol = Integer.parseInt(st.nextToken());
switch (protocol) {
case Function.LOGIN: {
String[] data = {
st.nextToken(),
st.nextToken(),
st.nextToken(),
st.nextToken()
};
wr.model2.addRow(data);
}
break;
case Function.MYLOG: {
card.show(getContentPane(), "WR");
}
break;
case Function.WAITCHAT: {
wr.ta.append(st.nextToken() + "\n");
}
break;
}
}
} catch (Exception ex) {}
}
//버튼 처리(요청)
@Override
public void actionPerformed(ActionEvent e) {
if (e.getSource() == login.b1) { //로그인버튼
String id = login.tf1.getText();
if (id.length() < 1) { //입력이 안 된 경우
JOptionPane.showMessageDialog(this, "ID를 입력하세요");
login.tf1.requestFocus();
return;
}
String name = login.tf2.getText();
if (name.length() < 1) { //입력이 안 된 경우
JOptionPane.showMessageDialog(this, "이름을 입력하세요");
login.tf2.requestFocus();
return;
}
String sex = "";
if (login.rb1.isSelected())
sex = "M";
else
sex = "F";
connection(id, name, sex);
} else if (e.getSource() == login.b2) { //취소버튼
dispose(); //메모리 삭제
System.exit(0); //정상종료
} else if (e.getSource() == wr.tf) { //채팅 들어가는 부분
String msg = wr.tf.getText(); //입력된 채팅 문자열 읽기
if (msg.length() < 1)
return;
try {
//서버로 전송
out.write((Function.WAITCHAT + "|" + msg + "\n").getBytes());
} catch (Exception ex) {}
wr.tf.setText(""); //채팅입력창 공백으로 초기화
} else if (e.getSource() == wr.b6) { //나가기 버튼 클릭
try {
out.write((Function.CHATEND + "|\n").getBytes());
//서버로 채팅을 종료한다고 요청
} catch (Exception ex) {}
}
}
public static void main(String[] args) {
try {
UIManager.setLookAndFeel("com.sun.java.swing.plaf.windows.WindowsLookAndFeel");
} catch (Exception ex) {
ex.printStackTrace();
}
new ClientMain();
}
}
모든 코드를 외워서 작성하려고 노력할 필요는 없다.
서버가 뭔지, 클라이언트가 뭔지, 왜 스레드를 사용하는지 정도의 개념을 확실히 익히면 나머지는 어떻게든 된다...
'Studying > Java' 카테고리의 다른 글
| 미니 네트워크 프로젝트 (1) : 화면 구성하기 (0) | 2022.06.23 |
|---|---|
| 입출력(I/O) : 직렬화(Serialization) (0) | 2022.06.15 |
| 입출력(I/O) : Stream, File (0) | 2022.06.14 |




