이 SwingUtils 클래스 안에는 method가 두개가 있다.
invokeLater 메소드와 invokeAndWait 메소드.
스윙은 스레드세이프하지않습니다 라고 선언을 해놓음.
이벤트가 발생한 순서에 따라서 event-queue.
큐는 들어오는 구멍과 나오는 구멍이 다른 데 있다.
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
큐모양
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
ui를 건들이는 코드들을 EDT한테 위임해라.
러너블 객체가 해야되는 일이 이벤크 큐에 넣겠다 -> 이렇게 하면 EDT가 수행함.
new MyFrame()을 결국 EDT가 한다.
EDT : event dispatch(ing) thread
-> 스윙은 거의 다 thread-safe 하지 않기 때문에.
-> 스윙과 관련된 작업들은 모두 이 쓰레드를 통해서 진행된다.
💡💡쉽게 얘기하면은
invokeLater(runnable객체) -> invokeLater 수행하는 스레드는 main이지만, runnable이 정의하고 있는 것은 EDT가 처리한다. 스윙의 안전성을 보장하겠끔 해놨다.
💡💡invokeAndWait(), invokeLater()을 사용하면, ()안의 runnable의 run()을 EDT가 수행하게 된다!!
invokeAndWait(b라는 일) : main이 b라는 작업을 이벤트큐에 잡아넣는다. 이건, b라는 작업넣어놓고, 수행완료할 때까지 기다린다. 일을 wait()가 시켰으니까 b라는 작업의 완료(이건 EDT가 한다.)를 기다린다. . =>join()과 비슷하다.
invokeLater(a라는 일) : main이 Later를 수행하면서, 이벤트큐에 a를 잡아넣어. 이때, 먼저 큐에 들어가있는 애들이 있을 수 있다. 먼저 들어간 애들 상관없이 a를
일을 시킨 시점과 일이 실제로 수행되는 시점이 다르다. (이미 들어간 애들이 있어서)
a가 수행되냐 안되냐는 상관없이 later은 자기일을 계속한다.
class MyFrame extends JFrame {
public MyFrame(String mode) {
System.out.println(mode + ":" + Thread.currentThread().getName());
try {
Thread.sleep(2000);
}catch(Exception e) {}
}
}
public class TestCase {
public static void main(String[] args) {
SwingUtilities.invokeLater(
new Runnable() {
public void run() {
new MyFrame("invokeLater");
}
}
);
System.out.println("invokeLater");
try {
SwingUtilities.invokeAndWait(
new Runnable() {
public void run() {
new MyFrame("invokeWait");
}
}
);
}catch(InterruptedException e ) {
}catch(InvocationTargetException e) {}
System.out.println("invokeWait");
new MyFrame("normal");
}
}
//결과
invokeLater
invokeLater:AWT-EventQueue-0
invokeWait:AWT-EventQueue-0
invokeWait
normal:main
클라이언트는 지맘대로 동작을 결정하면안된다.
chat에는 서버로부터 받은 것만 나와야한다.
똑같은 채팅을하는데 다른결과를 나타낼 수도 있다.
input은 전송만 돼야 한다.
끌때도 너 그래 꺼라 하고 꺼져야한다.
서버로부터 오더를 받아서 움직여야한다.
클라이언트만 바뀌고 나머지는 똑같음.
먜) 클라이언트에 Thread가 있는 이유.
계속 날아오는 메시지를 처리하기 위해 글을 읽는 부분을 스레드로 뺀다.
스레드를 활용하지 않으면, 글을 읽고 있을 때 상대방이 주는 메시지를
동시간으로 받지 못한다.
public class GUIChatClient extends JFrame implements ActionListener {
public static final int NORMAL = 0;
public static final int EXCEPTIONAL = -1;
private JTextField input;
private JTextArea display;
private BufferedReader br;
private PrintWriter pw;
private Socket sock;
public GUIChatClient() {
super("채팅 클라이언트");
init();
connect();
setDisplay();
addListeners();
showFrame();
}
private void init() {
input = new JTextField();
input.setBorder(new TitledBorder("Input"));
display = new JTextArea();
display.setEditable(false);
}
private void setDisplay() {
JPanel pnlCenter = new JPanel(new BorderLayout());
pnlCenter.add(new JScrollPane(display));
pnlCenter.setBorder(new TitledBorder("Chat"));
add(pnlCenter, BorderLayout.CENTER);
add(input, BorderLayout.SOUTH);
}
private void addListeners() { //루프를 돌지 않아도 되기 때문에, 리스너 써서 할 수 있다.
input.addActionListener(this);
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e ) {
pw.println("/quit");
pw.flush();
}
});
}
private void showFrame() {
setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
setSize(500,500);
setResizable(false);
setVisible(true);
input.requestFocus();
}
private void connect() { //연결.
String ip = null;
do {
ip = JOptionPane.showInputDialog(this, "아이피를 입력하시오");
}while (ip == null || ip.equals(""));
String id = null;
do {
id = JOptionPane.showInputDialog(this, "닉네임을 입력하시오.");
}while(id == null || id.equals(""));
try {
sock = new Socket(ip, 10001);
pw = new PrintWriter(new OutputStreamWriter(sock.getOutputStream()));
br = new BufferedReader(new InputStreamReader(sock.getInputStream()));
pw.println(id.trim());//id읽으려고 준비중.
pw.flush();
WinInputThread wit = new WinInputThread();
wit.start();
}catch(Exception e) {
System.out.println("서버와 접속시 오류가 발생하였습니다.");
System.out.println(e);
System.exit(EXCEPTIONAL);
}
}
@Override
public void actionPerformed(ActionEvent e) {
if(e.getSource() == input) {
String msg = input.getText();
pw.println(msg);
pw.flush();
input.selectAll();
input.requestFocus();
}
}
class WinInputThread extends Thread{
public void run() {
try {
String line = null;
while((line = br.readLine()) != null) { //서버로부터 읽어온다.
if(line.equals("/quit")) {
throw new Exception(); //예외 던져서 끝내기.
}
display.append(line + "\n"); //붙여넣기
//커서 위치 조절(스크롤문제)
display.setCaretPosition(display.getDocument().getLength()); //커서위치잡는 거=>caretPosition
}
}catch(Exception e ) {
System.out.println("client thread :" + e);
JOptionPane.showMessageDialog(GUIChatClient.this, "프로그램을 종료합니다");
}finally {
try {
br.close();
}catch(Exception e) {}
try {
pw.close();
}catch(Exception e) {}
try {
sock.close();
}catch(Exception e) {}
System.exit(NORMAL);
}
}
}
public static void main(String[] args) {
SwingUtilities.invokeLater(
new Runnable(){
public void run() {
new GUIChatClient();
}
}
);
}
}
public class ChatServer {
public static void main(String[] args) {
try {
ServerSocket server = new ServerSocket(10001);
System.out.println("접속을 기다립니다.");
HashMap<String, PrintWriter> hm = new HashMap<String, PrintWriter>();
//이건 id랑pw연결해주는 거. 클라이언트를 담당하는 스레드가 하나씩 있음. -> 접속해있는 클라이언트들한테 쓸 수 있는 println()하려면 pw있어야 한다.
while(true) {
Socket sock = server.accept();
ChatThread chatthread = new ChatThread(sock, hm); //
chatthread.start();
}//while
}catch(Exception e) {
System.out.println("server main : " + e);
}
}//main
}
public class ChatThread extends Thread{
private Socket sock;
private String id;
private BufferedReader br;
private HashMap<String, PrintWriter> hm;
public ChatThread(Socket sock, HashMap<String, PrintWriter> hm) {
this.sock = sock;
this.hm = hm;
try {
PrintWriter pw = new PrintWriter(new OutputStreamWriter(sock.getOutputStream()));
br = new BufferedReader(new InputStreamReader(sock.getInputStream()));
id = br.readLine(); //여기서 멈춘다.
broadcast(id + "님이 접속하였습니다."); //나한테는 안온다 나는 26번줄에 들어오니까.
System.out.println("접속한 사용자의 아이디는" + id +"입니다");
synchronized (hm) { //클라이언트 id..가 해쉬맵에 들어간다.
hm.put(id,pw);
}
}catch(Exception ex) {
System.out.println("server thread constructor:" + ex);
}
}//생성자
public void run() {
try {
String line = null;
while((line = br.readLine())!=null && !line.equals("/quit")) { //quit이면 false돼서 반복문 끝남.
if(line.indexOf("/to ") == 0){ //to가 0번 인덱스에 있다면
sendmsg(line);
}else {
broadcast(id + ":" + line);
}
}
}catch(Exception ex) {
System.out.println("server thread run :" + ex);
}finally {
PrintWriter pw = null;
synchronized (hm) {
pw = hm.remove(id);
pw.println("/quit");
pw.flush();
}
String info = id + "님이 접속을 종료하였습니다.";
broadcast(info);
try {
pw.close();
}catch(Exception e) {}
try {
br.close();
}catch(Exception e) {}
try {
sock.close();
}catch(Exception e) {}
}
}//run
//sendmsg
public void sendmsg(String msg) {
int start = msg.indexOf(" ")+1; //to 다음 공백+1 'd'이라는 거.(/to doo hi~) index=4
int end = msg.indexOf(" ",start);// 'doo'의 앞 뒤 공백.
if(end != -1) { //공백 발견했따.
String to = msg.substring(start, end);//4번부터 6번까지
String msg2 = msg.substring(end + 1);//8번부터 끝까지
synchronized (hm) {
PrintWriter pw = hm.get(to); //doo를 뜻함. 사용자에게 쓸 수 있는 pw를 구해와라.
String resultMsg = "귓말전송에 실패했습니다.";
if(pw != null) { //doo가 key 존재하지 않을 때. doo라는 사람 없는데? 하면 귓말 실패했다. 있다면 밑에코드..
pw.println(id + "님이 다음의 귓속말을 보내셨습니다 : " + msg2); //자기id
pw.flush();
resultMsg = to + "님께서 다음의 귓속말을 보냈습니다." + msg2; //doo님께 보냈습니다.
}
pw = hm.get(id); //자기 id에게 귓말전송 실패했다고 보낸다.
pw.println(resultMsg);
pw.flush();
}
}
}
//
public void broadcast(String msg) {
synchronized (hm) {
Collection<PrintWriter> collection = hm.values();//맵에 들어가있는 모든 클라이언트에게 write할 수 있는 pw다. "나"도 포함.
Iterator<PrintWriter> iter = collection.iterator();
while(iter.hasNext()) {
PrintWriter pw = iter.next();
pw.println(msg);
pw.flush();
}
}
}
}
3월 23일, 3월 24일