# 聊天室 ###### 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)。 :::