通過一個(gè)最簡單的例子來看服務(wù)端編程的流程
在了解服務(wù)端編程的流程之前,我們需要先了解幾個(gè)函數(shù):
1.創(chuàng)建套接字的函數(shù)(關(guān)于套接字的介紹在上一節(jié)中有了簡單的描述,其實(shí)就是一個(gè)文件描述符,也就是一個(gè)整數(shù))
首先我們可以在Linux終端輸入:man 7 socket來查看系統(tǒng)對這個(gè)函數(shù)的描述文檔。
從以上描述我們可以看到,你要使用這個(gè)函數(shù),首先要包含sys/socket.h頭文件。
然后可以看到,這個(gè)函數(shù)有三個(gè)參數(shù):
a.第一個(gè)socket_family表示地址族,這個(gè)我們一般用的就是AF_INET,表示ipv4地址族。
b.第二個(gè)參數(shù)socket_type,從名字我們就可以看出來,是套接字的類型,上節(jié)也提到了套接字主要有兩種類型,一種是流格式的套接字(SOCK_STREAM),另一種是數(shù)據(jù)報(bào)格式的套接字(SOCK_DGRAM),分別對應(yīng)TCP協(xié)議和UDP協(xié)議。
c.第三個(gè)參數(shù)協(xié)議,IPPROTO_TCP表示使用的是TCP協(xié)議
調(diào)用這個(gè)函數(shù),用一個(gè)整型接收他的返回值,我們就得到了一個(gè)套接字。
2.bind函數(shù),通過這個(gè)函數(shù)將套接字與IP和端口等進(jìn)行綁定。
同樣我們可以在終端中鍵入:man 2 bind命令來查看相關(guān)文檔
這個(gè)函數(shù)也有三個(gè)參數(shù):
a.第一個(gè)就是我們通過socket函數(shù)得到的套接字,也就是你要綁定的套接字。
b.第二個(gè)就是一個(gè)sockaddr結(jié)構(gòu),我們只需要知道里面有幾個(gè)重要的成員就可以了,比如port addr family 分別表示端口,IP地址,地址族等。
c.第三個(gè)參數(shù)就是第二個(gè)結(jié)構(gòu)的長度。
這個(gè)函數(shù)的返回值表示這個(gè)函數(shù)是否執(zhí)行成功,具體細(xì)節(jié)可以查看man文檔
3.listen函數(shù),這個(gè)函數(shù)就是用來監(jiān)聽套接字的,監(jiān)聽有沒有人來連接你套接字綁定的地址和端口。
同樣man listen查看man文檔
這個(gè)函數(shù)有兩個(gè)參數(shù):
a.第一個(gè)參數(shù)是我們通過socket函數(shù)拿到的套接字
b.第二個(gè)參數(shù)表示能連接到這個(gè)套接字的最大數(shù)值。可以自己設(shè)定
4.accept函數(shù),這個(gè)函數(shù)用來接收客戶端連接請求
man accept查看man文檔
三個(gè)參數(shù):
a.第一個(gè)是服務(wù)端的套接字
b.第二個(gè)要注意,這次是客戶端的sockaddr結(jié)構(gòu),它是一個(gè)傳出參數(shù),也就是你傳一個(gè)這種結(jié)構(gòu)的地址進(jìn)去,函數(shù)調(diào)用結(jié)束之后,這個(gè)結(jié)構(gòu)就被賦值了。你就可以拿來用了。
c.第三個(gè)參數(shù)是第二個(gè)參數(shù)結(jié)構(gòu)的長度
它的返回值也要注意,它返回的是連接服務(wù)器的客戶端的套接字,我們可以用一個(gè)整型來進(jìn)行接收,我們之前說過,套接字其實(shí)就是一個(gè)文件描述符,我們得到了這個(gè)文件描述符之后,就可以對這個(gè)描述符進(jìn)行文件操作了,比如給它寫幾個(gè)數(shù)據(jù),就相當(dāng)于給客戶端發(fā)送了數(shù)據(jù)。
接下來是服務(wù)端的代碼:
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <sys/types.h> #include <unistd.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> int main() { //創(chuàng)建一個(gè)套接字,服務(wù)端 int sock_serv = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //AF_INET表示的是ipv4地址族, //第二個(gè)參數(shù)SOCK_STREAM表示的是套接字類型:流格式的套接字,第三個(gè)參數(shù)表示使用的協(xié)議是TCP //將套接字和IP、端口等綁定 struct sockaddr_in sockaddr_serv; memset(&sockaddr_serv, 0, sizeof(sockaddr_serv)); sockaddr_serv.sin_family = AF_INET; sockaddr_serv.sin_port = htons(1234); sockaddr_serv.sin_addr.s_addr = inet_addr("127.0.0.1"); //這個(gè)函數(shù)的第一個(gè)參數(shù)是套接字(整型),第二個(gè)參數(shù)是用來初始化端口和IP的結(jié)構(gòu)(注意要進(jìn)行強(qiáng)制轉(zhuǎn)換),第三參數(shù)就是結(jié)構(gòu)的大小了 bind(sock_serv, (struct sockaddr*)&sockaddr_serv, sizeof(sockaddr_serv)); //監(jiān)聽套接字,看網(wǎng)絡(luò)上有多少個(gè)主機(jī)會來和這個(gè)套接字進(jìn)行連接 listen(sock_serv, 20); //第二個(gè)參數(shù)是能連接這個(gè)套接字的最大數(shù)量 //接收客戶端請求 struct sockaddr_in sockaddr_client; socklen_t socklen_client = sizeof(sockaddr_client); int sock_client = accept(sock_serv, (struct sockaddr*)&sockaddr_client, &socklen_client); //accept的第三個(gè)參數(shù)是傳出參數(shù),也就是把這個(gè)參數(shù)傳給函數(shù)accept函數(shù)之后,函數(shù)會對這個(gè)參數(shù)進(jìn)行賦值,函數(shù)調(diào)用完之后 //這個(gè)參數(shù)就有值了,可供調(diào)用者使用。 //向客戶端寫數(shù)據(jù) char str[] = "I am a boy!"; write(sock_client, str, sizeof(str)); //最后要記得關(guān)掉套接字,也就是要關(guān)掉文件描述符 close(sock_serv); close(sock_client); return 0; }
上面的代碼中,我們給服務(wù)端sockaddr結(jié)構(gòu)賦值的時(shí)候用的是sockaddr_in,所以在賦完值之后準(zhǔn)備傳給bind函數(shù)時(shí)要進(jìn)行強(qiáng)制轉(zhuǎn)換。這里的服務(wù)端IP用的是本地回環(huán)測試IP 127.0.0.1 也就是把本機(jī)當(dāng)作服務(wù)器。
然后就是客戶端:
客戶端我們要了解一個(gè)函數(shù)connect:
從這個(gè)函數(shù)名我們都可以知道,這是用來連接服務(wù)器的函數(shù),我們可以想一想,要連接服務(wù)器,我們要如何找到服務(wù)器呢,當(dāng)然是通過IP和端口啦。這個(gè)IP和端口也是通過sockaddr結(jié)構(gòu)來給的。
通過man 2 connect查看man文檔
這個(gè)函數(shù)也有三個(gè)參數(shù):
a.第一個(gè)參數(shù)是客戶端的套接字,客戶端也是需要用socket函數(shù)創(chuàng)建套接字的,因?yàn)榉?wù)端調(diào)用accept函數(shù)得到的是客戶端的套接字,如果服務(wù)器給客戶端寫了數(shù)據(jù),客戶端就可以通過這個(gè)套接字來讀到。
b.第二個(gè)參數(shù)是sockaddr結(jié)構(gòu),注意這個(gè)結(jié)構(gòu)是服務(wù)端的信息。
c.第三個(gè)參數(shù)是第二個(gè)參數(shù)的長度。
下面是客戶端的代碼:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <unistd.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> int main() { //客戶端也要?jiǎng)?chuàng)建一個(gè)套接字 int socket_client = socket(AF_INET, SOCK_STREAM, 0); //設(shè)置要連接的服務(wù)器的IP 端口 地址族等 struct sockaddr_in sockaddr_serv; memset(&sockaddr_serv, 0, sizeof(sockaddr_serv)); sockaddr_serv.sin_addr.s_addr = inet_addr("127.0.0.1"); sockaddr_serv.sin_family = AF_INET; sockaddr_serv.sin_port = htons(1234); //客戶端連接服務(wù)器 connect(socket_client, (struct sockaddr *)&(sockaddr_serv), sizeof(sockaddr_serv)); //讀取服務(wù)器傳回的數(shù)據(jù) char str[50]; read(socket_client, str, sizeof(str) - 1); //回顯到終端輸出 printf("get the data from server: %s\n", str); close(socket_client); return 0; }
要實(shí)現(xiàn)效果我們是要開兩個(gè)終端的,因?yàn)榉?wù)端代碼啟動(dòng)之后是會進(jìn)入阻塞狀態(tài)的,等待客戶端的連接。先啟動(dòng)服務(wù)端,再啟動(dòng)客戶端。
1.啟動(dòng)服務(wù)器
我們可以看到,執(zhí)行服務(wù)端程序之后,程序阻塞了。
2.然后啟動(dòng)客戶端
我們可以看到,服務(wù)端給客戶端寫的數(shù)據(jù),客戶端讀了之后,回顯到終端了。