# 聊天室
###### tags: `project 2`
## ~~1st~~2nd討論
### 分工
~~先處理server、client一對一溝通的問題~~
~~最後再看看server要怎麼整合~~
都有各自的server client
最後上傳某個人的code
各自問題排解
### 架構
* 1個client對應1個thread(socket)
* 一個server對應多個clients
* 用queue或list儲存client傳給server的data
* 一個多人聊天室,進入前先輸入使用者名稱傳給server
### Problem
1. Socket timeout問題
>[color=#FF00FF]在socket進行recv的時候是可以無限期的blocking的,這時候會讓thread直接卡住,可以用select()這個function來解決這個問題,詳細請自行查閱資料。
>還有一個function叫做poll,也是用來進行類似的功能,只是兩個的實作跟各自的限制不太一樣。
>這是比較困難一點的部分,建議是一般的recv先會用之後再考慮加上select()。
>但是當某條thread只有一個socket要監聽,並且只做監聽這件事情的時候,一個blocking的recv或是looping的select似乎是前者較有效率,不過效率的問題還是只有在比較critical的環境下才會浮上檯面,像是目標有上千個或是硬體裝置相當受限。
>[name=Neko]
>
2. lock等方式避免queue或list 內容的race condition
3. queue或list 反覆check內容可能耗時間
4. GUI、TUI顯示的細節
5. ~~虛擬機、host彼此連線之問題~~
>[color=#00a000]
>
>目前我和佩昌參考的是[這份code](https://www.itread01.com/content/1533623883.html)
>
>2好像真的有點事,
>似乎有race condition
>但是還沒看到deadlock出現過
>**critical section有待判定**
>
>4的話頂多打字畫面被干擾,但輸入內容(不含更改)不會被其他人中斷
> [name=Omnom]
>使用wifi時要確認虛擬機網路的狀態
>port不能太小
>結果我還是不能當server ==
>[name=Omnom][color=#00A000]
>
>今天使用tmux進行同步輸入訊息,這份code是沒有任何問題的
>只是說如果是一起登入或登出會有機率讓server出現小問題,登入時有可能會沒有顯示xxx enter the chat room,登出時則是有可能直接停止程式
>[name=Conan][color=#0000ff]
6. Socket read write in different thread?
>[color=#FF00FF]Sockets of type SOCK_STREAM are full-duplex byte streams, similar to pipes. --[reference](https://linux.die.net/man/2/socket)
>結論,一個SOCK_STREAM的socket檔案描述符在不同的執行序中讀寫是安全的。
>[name=Neko]
7. Username跟message要怎麼傳給server呢? 就封包格式來說。
>[color=#FF00FF]目前在研究如何在socket上傳遞struct data
>似乎是要做serialize之類的。
>[name=Neko]
>[color=#FF00FF][ref](https://stackoverflow.com/questions/16543519/serialization-of-struct)
>簡單來說就是將struct的各個attribute以特定的順序將所有的bytes串接在一起,形成char array,透過socket傳遞,接收方要按照相同的順序將bytes拆開,各自解析。
>[name=Neko]
8. ctrl+C的問題
>[color=#00a000]已經和佩昌成功解決server與client ctrl+C的問題
>[name=Omnom]
### document內容
上傳某個人的code
不同version的問題描述
---
## 程式筆記
:::spoiler
## Makefile 寫法
以下範例示範如何編譯 `main.c` 這份文件,可以看到要生成main 的執行檔之前,會先要求執行 `main.c` 的編譯,編譯得到 `main.o` 在編譯成執行檔 `main` 。
而clear 可以清除多餘的 `.o` 檔等文件。
> gcc foo1.c -o foo1
事實上,上面的這個編譯方式可以拆解成:
gcc foo1.c -c
gcc foo1.o -o foo1
編譯的過程是將原始碼(foo1.c)先利用-c參數編譯成.o(object file),然後再鏈結函式庫成為一個binary。-c即compile之意。
```makefile
main: main.o
gcc main.o -o main
main.o: main.c
gcc main.c -c
clean:
rm *.o
```
## **參考**
[Makefile範例教學](http://maxubuntu.blogspot.com/2010/02/makefile.html)
## Socket 用法
### 參考下面文章
[TCP Socket Programming 學習筆記](http://zake7749.github.io/2015/03/17/SocketProgramming/)
### 建立socket
```c
int socket(int domain, int type, int protocol);
sockfd = socket(AF_INET, SOCK_STREAM, 0);
```
這邊 **domain 選用** `AF_INET` 可以讓兩台主機以網路(IPv4)來傳輸資料,另有`AF_INET6`使用`IPv6`協定傳輸。
**type 選擇** `SOCK_STREAM` 使用 TCP 為 protocol 傳輸資料。
protocol 預設通常為0,讓kernel選擇type對應的默認協議。
最後會回傳 socket 的檔案描述符(socket file descriptor),我們可以透過他來操作 socket ,如果建立失敗會回傳 -1。
### socket 功能(client 端)
- connect()
功能: 項目標IP 位置產生連線。
```c
struct sockaddr_in info;
bzero(&info, sizeof(info));
info.sin_family = PF_INET; //socket 為 IPv4 結構
//server位置
info.sin_addr.s_addr = inet_addr("127.0.0.1");
info.sin_port = htons(8700);
// 開始連接
int err = connect(sockfd, (struct sockaddr *)&info, sizeof(info));
```
connect() 需要的參數如下:
1. socket file descriptor ,也就是剛剛創建的`socket`。
2. 連接的資訊(型態為 `sockaddr_in`,包含 `IP`格式、`IP`與 `port`)。
3. 資訊的大小 (型態為`socklen_t`)。
特別介紹 `htons` ,他是host to network short 的簡稱,因為傳輸雙方可能會因為`big-endian` 或是 `little-endian` 而出現錯誤(Intel 的架構比較特殊) ,因此需要特別轉換。
[9.12. htons(), htonl(), ntohs(), ntohl() - Beej's Guide to Network Programming 正體中文版](http://beej-zhtw.netdpi.net/09-man-manual/9-12-htons-htonl-ntohs-ntohl)
- recv()
功能: 用於接收訊息。
```c
char buf[255] = {};
int ret = recv((int)sockfd, buf, sizeof(buf), 0);
```
recv() 需要的參數如下:
1. socket file descriptor ,也就是剛剛創建的`socket`。
2. 接收訊息的 buffer 。
3. buffer 的大小。
4. flag 通常為0。
最後會回傳接收到了**多少個位元組**,若在接收時發生的**錯誤則會傳回-1**。
[recv(2) - Linux manual page](https://man7.org/linux/man-pages/man2/recv.2.html)
>[color=#FF00FF]TCP保證了接收封包的順序,但是如果一筆資料很大,拆成很多個封包傳輸,要如何確保已經接收完一個完整封包呢?
>[請參考](http://code.activestate.com/recipes/408859-socketrecv-three-ways-to-turn-it-into-recvall/)
>[name=Neko]
- send()
功能: 用於傳送訊息。
```c
char message[255] = {};
send(sockfd, message, sizeof(message), 0);
```
send() 需要的參數如下:
1. socket file descriptor ,也就是剛剛創建的`socket`。
2. 傳送的訊息 message。
3. message 的大小。
4. flag 通常為 0。
最後會回傳傳送**多少個位元組**,若在接收時發生的**錯誤則會傳回-1**。
### Socket 功能(server 端)
- bind() & listen()
功能: 相較於client 的 connect() ,server 這邊的做法是使用bind() 將IP 綁在socket 上,讓別人知道這個IP 有個socket,而要知道有沒有人要求連線則是使用listen()。
```c
// socket的連線
struct sockaddr_in serverInfo;
socklen_t addrlen = sizeof(clientInfo); //傳輸資料的長度
bzero(&serverInfo, sizeof(serverInfo));
serverInfo.sin_family = PF_INET;
serverInfo.sin_addr.s_addr = INADDR_ANY;
serverInfo.sin_port = htons(8700); // socket 的 port
// 綁定自身位置
bind(sockfd, (struct sockaddr *)&serverInfo, sizeof(serverInfo));
listen(sockfd, 5);
```
bind() 需要的參數如下:
1. socket file descriptor ,也就是剛剛創建的`socket`。
2. 連接的資訊(型態為 `sockaddr_in`,包含 `IP`格式、`IP`與 `port`),其中`INADDR_ANY` 代表讓kernel 自己決定 local IP 的位置。
3. 資訊的大小。
最後回傳 0表示綁定成功,-1則表失敗。
listen() 需要的參數如下:
1. socket file descriptor ,也就是剛剛創建的`socket`。
2. 預計監聽socket 的數量。
最後回傳 0表示監聽成功,-1則表失敗。
- accept()
功能: 接受來自client 端的連線
```c
struct sockaddr_in clientInfo;
//接收來自client 的訊息
forClientSockfd = accept(sockfd, (struct sockaddr *)&clientInfo, &addrlen);
```
accept() 需要參數如下:
1. socket file descriptor ,也就是剛剛創建的`socket`。
2. 連接的資訊(型態為 `sockaddr_in`),用來接收client 的連接資訊。
3. 資訊的長度。
最後回傳一個新的Socket描述符,以後和Client端交談的是這個新創出的Socket,如果失敗則傳回-1(INVALID_SOCKET)。
:::