http是基於tcp的協議,在發送http請求之前,要先與伺服器建立tcp連接,然後才可以發送HTTP請求。
HTTP請求的頭部,就是一些以\r\n分割的字符串。
第一行為GET、POST方法,之後的每一行為冒號分割的鍵值對,表示http請求的一些信息。
最後以一行單獨的\r\n與請求的正文分割。
如果沒有正文,則最後這行\r\n就表示請求的結束。
它與上一行的\r\n正好構成連續的2個回車換行,這可以用來判斷http頭的結束和正文的開始。
把這行字符串發送到伺服器,然後讀取返回結果,就是伺服器的應答。
應答可能是個html文件,也可以是其他文件,或者一個flv的動態視頻流(直播一般使用http-flv)。
實際的工程代碼中,為了避免connect連接期間無法響應用戶操作,一般使用異步socket。
只需給epoll_wait設置10毫秒超時,用戶是感知不到的,但是同步socket多長時間返回是不確定的。
Linux異步socket的步驟:
1,在socket()函數獲取文件描述符時設置SOCK_NONBLOCK標示,也可以在之後用fcntl()函數設置。
2,connect()函數將返回-1並提示EINPROGRESS的錯誤碼,表示連接正在進行。
3,把socket文件描述符加入epoll監控寫事件EPOLLOUT。
4,在epoll提示可寫時,用getsockopt在SOL_SOCKET層上讀取SO_ERROR錯誤碼,0表示連接成功,-1表示失敗。
tcp連接成功之後,就可以構造一個http的請求字符串,發送給伺服器了。
之下幾張圖是演示的代碼。
先創建一個epoll的文件描述符,然後創建一個異步socket。
domain參數為AF_INET,表示ipv4。
type參數為SOCK_STREAM表示tcp,SOCK_NONBLOCK表示非阻塞。
最後一項參數為0。
然後設置連接地址,圖中是我本機的nginx伺服器地址。
16位埠使用網絡字節序(大端),用htons()轉換。
IP位址用inet_addr()轉換,這裡是本機的本地環回地址127.0.0.1。
調用connect()連接,如果返回值為-1時查看errno是否為EINPROGRESS,不是則出錯。
這裡不大可能直接返回0表示連接成功,因為要等待伺服器響應,有個TCP三次握手過程,而connect()函數發送了TCP_SYN包就要返回,收到伺服器的TCP_ACK包(一般同時設置SYN標示)才算成功。伺服器端則要再收到客戶端的TCP_ACK包才認為成功。
epoll監控它的讀事件EPOLLIN、寫事件EPOLLOUT、對端關閉事件EPOLLRDHUP。
EPOLLET,邊沿觸發,即socket的讀寫狀態有變化時觸發事件。
EPOLLLT,默認是水平觸發,即可讀可寫時一直觸發事件。
常用的是EPOLLET,邊沿觸發,所以可讀時要一次讀完socket緩衝區的數據,直到recv()提示EAGAIN錯誤碼。
可寫時也要一次寫完數據,或者send()提示EAGAIN錯誤碼。
while循環為epoll的主循環,不斷的使用epoll_wait監控讀寫事件。
返回值< 0表示出錯,= 0表示超時。
這裡只有一個文件描述符需要監控,正常時返回1,不需要遍歷了,加個assert斷言讓代碼邏輯保持完整就行。
這兩個assert如果真被觸發了?!
那一定是我手抖了,把==打成了!=(笑
如果是還沒連接成功,讀取連接狀態,err碼為0表示成功,其他表示失敗。
連接成功之後,組裝一個http的請求頭,去GET nginx伺服器的根目錄/。
只需要設置這麼幾項就行,見代碼圖片。
在socket可寫時把它發送到伺服器,不一定能一次發送完,所以記錄總長度len和發送的偏移量pos。
如果send()出錯,這裡只能是EAGAIN或者EINTR,一個表示socket不可寫需要等下次發送,一個表示send()函數被打斷也是需要再次發送,其他錯誤碼都是真出錯了。
發送成功之後,根據epoll提示的讀事件,讀取伺服器的響應。
一次不一定能讀完,這裡要解析http響應頭來確定讀取結果。
先獲取Content-Length項的長度,然後判斷http正文的長度,如果這兩個相等則讀完了,否則要繼續讀。
如果是chunk模式,則根據每個chunk前的長度值確定該chunk的大小,最後一個chunk大小是0。
這裡直接把第一次recv到的數據打出來,沒有進一步解析。
讀取到的結果,本機的nginx,一次就讀全了。