學(xué)完C語言做不出東西?不存在的,咱們做一個最“隱私”的聊天器,就倆人,你和我。咱們聊天的信息你知我知沒別人知。
我們直接開始寫代碼,只要你會基礎(chǔ)的C語言,不要擔心看不懂,不懂的我?guī)湍闩俑鶈柕?,把根都挖出來嚼爛,絕對懂。
一、一個聊天軟件的基礎(chǔ)模型是怎么樣的?
你是個新手的話你可能就會問,什么是模型?!聽不懂,我在騙你學(xué)習(xí)。放心,我現(xiàn)在就告訴你什么是基礎(chǔ)“模型”。
我們可以簡單的理解“模型”指這個聊天軟件基本是怎么進行通信的,常規(guī)形式是怎樣的,只要清楚了這個形式流程,然后在這個流程中添加一些代碼就ok了,啥都不用想。如果你還是不懂什么是“流程”,那我就跟你說這個是一個步驟,只需要懂這個步驟,我們使用代碼編寫這個步驟就可以完成了。
好了,現(xiàn)在沒啥問題了吧?現(xiàn)在開始,第一步在一個通信中,一般有一個服務(wù)端。那什么是服務(wù)端?
1.1 什么是服務(wù)端
服務(wù)端就簡單了,曾經(jīng)…曾經(jīng)…你去例如移動或者聯(lián)通的營業(yè)挺,客服小姐姐就會對你提供服務(wù),例如業(yè)務(wù)辦理,辦個卡,銷個號等,那我們的服務(wù)端是用來通信的,所以這個服務(wù)端就是指等待跟我聊天的人,只要你上線了,開電腦打開軟件了,連接上我的服務(wù)端了,咱們就可以聊天了。
服務(wù)端一般就是一直在這里等你上線的那個,風(fēng)里雨里我在這里等你。
1.2 又不懂什么是客戶端了?
不懂沒關(guān)系,打游戲懂吧?你下載到你電腦你手機的就是客戶端,你打個游戲如果沒有服務(wù)端就不能跟人匹配,這個懂了吧?
1.3 基本的工具要拿過來吧?
還知道頭文件吧?
頭文件就等于是一個工具箱,需要干啥就可以使用拿頭文件過來,這樣就可以用里面的工具了。
那咱們做一個聊天的軟件就需要一個工具箱吧,這個工具箱叫做“Winsock2.h”,那怎么拿呢?都知道#include<> 吧?
那就直接把這個頭文件拿過來就好了,代碼就可以寫成:#include<WinSock2.h>。
常規(guī)的輸入輸出工具箱也要拿吧?所以就第一步把 stdio.h 也拿過來,所以這個服務(wù)端的第一行第二行代碼就寫成:
#include<stdio.h> #include<WinSock2.h>
1.4 開始 socket 編程
不會了不會了!是不是一說 socket 你就說這是個什么鬼?
我先說一句讓你懵逼的定義“socket 就是應(yīng)用之間通信的端點”。懂不懂?
不懂呀,那我繼續(xù)說。
socket 就是兩個通信軟件之間的接口,你可以當成服務(wù)端是“插座”,客戶端是“插頭”,一插,歐了!這樣不就通電了,這樣說你明白了吧?
當然這樣解釋比較片面,但用“抽象”的方式講又不一定能讓大家聽得懂,所以你就理解成插頭肯定沒問題。
1.5 開始抬杠我拿三座插兩座插不進!
咱們用的插頭都是有標準的,你想想,沒有標準怎么那么多電器都可以用常規(guī)的插頭?
像這個 socket 這個通信端口,是有基于一些標準的。例如 TCP/ip這些通信協(xié)議。
好了,我說了TCP/IP可能就會有同學(xué)問,這又是什么鬼!沒關(guān)系,你只需要知道這個是一個通信協(xié)議,咱們現(xiàn)在是用 socket 進行通信就好,知道 socket 怎么用就行,協(xié)議咱們可不需要現(xiàn)在搞懂,咱們只需要知道 socket 如何運用即可。
二、開始敲服務(wù)端代碼
2.1 搞清楚使用 socket 進行通信的步驟
編寫C語言Windows下的socket需要經(jīng)過幾個步驟:首先對WSAStartup 進行初始化,初始化對socket 套接字(socket也叫套接字)進行創(chuàng)建,隨后配合綁定信息,接著進行配置信息的bind 綁定;綁定了信息后,通過該信息進行isten 監(jiān)聽,監(jiān)聽后若有鏈接則connect 連接,再接下來開始使用accept 接收請求,得到請求后可以選擇接受recv或者send發(fā)送數(shù)據(jù),最后closesocket 關(guān)閉 socket,WSACleanup 最終關(guān)閉。
簡單點就是下面的這個流程:
不懂了?不懂就慢慢來嘛。
這是進行 socket 編程的步驟,如果你要問為什么要這樣做…我只能回答你規(guī)定的流程就這樣,因為你要進行通信,那肯定需要創(chuàng)建一個 socket ,創(chuàng)建完畢后那么肯定要綁定你要通信的信息,如果你不綁定你怎么知道你要跟誰說話呢?急著我收到了一個信息后就等于跟我請求通話,我同意了,咱們就開始通信了,通信肯定要發(fā)送信息,那就用send這些方法發(fā)送了,最后面說完話我就關(guān)閉這個 socket了,那你說不是嗎?
還不懂?那你看下面。
2.1 第一步初始化
既然第一步是初始化,那我要初始化什么東西?
我們需要初始化一個 WSADATA 類型數(shù)據(jù)的對象。
什么鬼?又是 wsaData 又是對象的,聽不懂?。?br />沒關(guān)系的拉,WSADATA 其實就是一個結(jié)構(gòu)體,咱們在把使用socket的工具箱 WinSock2 拿過來的時候這個 WSADATA 結(jié)構(gòu)體就已經(jīng)創(chuàng)建好了,直接使用這個結(jié)構(gòu)體創(chuàng)建一個結(jié)構(gòu)體變量就好了。
WSADATA 的作用就是用來存儲初始化信息的,就像你打個游戲初始化創(chuàng)建一個人,這個人總得有信息吧,光頭、小眼睛、腿短…對吧?
那么我們的代碼就可以寫成以下:
#include<stdio.h> #include<WinSock2.h> #include <stdlib.h> int main(){ WSADATA wsaData; }
接下來就可以開始初始化了,初始化 socket 有一個函數(shù)叫做 WSAStartup,既然是函數(shù)一般都有參數(shù)吧,參數(shù)有哪些呢?
這個 WSAStartup 方法需要傳入一個 版本號,還有一個用于存儲信息的 WSADATA 結(jié)構(gòu)體?,F(xiàn)在我們已經(jīng)知道 WSADATA 的結(jié)構(gòu)體就是上面這個代碼創(chuàng)建的 wsaData 結(jié)構(gòu)體變量,那么版本號又是什么?
這個版本號是說明我們使用哪個 Winsock 版本,Winsock 有一個 1.1 版本還有一個 2.2 版本。兩個版本有不同,1.1 版本只支持 TCP/IP 協(xié)議,還有一個版本 2.2 支持多個協(xié)議,這個時候你懂用哪個了吧?
什么?! 還不懂? 那肯定是全都要呀!
2.2 版本兼容性之類的更好,兼容啥我們不管,反正用多的。
那直接寫成 WSAStartup(2, 2, &wsaData)?
不不不,我們寫法有一些不同,需要用一個函數(shù) MAKEWORD 對版本進行生成,就像這樣 WSAStartup(MAKEWORD(2, 2), &wsadata);,規(guī)定咱們使用 MAKEWORD 告訴 WSAStartup 初始化調(diào)用什么版本。
那么整個初始化的代碼就如下所示咯:
#include<stdio.h>#include<WinSock2.h>#include <stdlib.h>int main(){ WSADATA wsaData; WSAStartup(MAKEWORD(2, 2), &wsadata);}
什么?不懂 &wsadata ?來來來,我們的漫畫同學(xué)告訴你是啥意思:
懂了吧?傳個地址方便信息存儲。
2.2 第二步創(chuàng)建 socket
這一步超級簡單,代碼就是這個:
SOCKET serverSock = socket(PF_INET, SOCK_STREAM, 0);
我知道你要罵我,寫什么是什么鬼。
好了好了,首先 SOCKET 是一個socket的類型,還記得 int a 吧?int 是一個類型,那么 SOCKET 肯定就是一個類型了,說明創(chuàng)建一個 SOCKET 類型的變量,然后 socket() 是創(chuàng)建 socket 的函數(shù),這個沒毛病吧?
你說是里面的參數(shù)不懂?
小問題了,第一個 PF_INET 就表示指定 IPv4 ,也就是說先給個網(wǎng)絡(luò)協(xié)議,那么多的網(wǎng)絡(luò)協(xié)議你總要選一個吧。那為什么要用 IPv4 呢?我只能說用這個東西計算更快,畢竟咱們做個聊天軟件是局域網(wǎng)通信,你就理解為,咱們做的東西是個“小東西”,沒必要那么大“體量”,迷你更好用,那就用那個 IPV4 了,你想不開你也可以用 IPV6 試試。
那 SOCK_STREAM 是什么?SOCK_STREAM 表示咱們進行的通信是 TCP 通信,穩(wěn)定可靠。在這里使用 SOCK_STREAM 也表示向我們的系統(tǒng),或者你理解成“計算機”申請一個通信的端口,不然系統(tǒng)不給你“開個口子”,我的數(shù)據(jù)怎么傳出去對吧,不然就是叫破喉嚨都沒人理我。
那最后一個參數(shù) 0 又是什么呢?
這里就是一個編號,說仔細點這個是 socket 所使用的傳輸協(xié)議編號,是不是不明白?其實這就是一個編號,不做設(shè)置,但是要給一個值,所以就給一個 0 咯。
2.3 第三步綁定信息
綁定信息這一步就有點玄了。在這里咱們要了解兩個結(jié)構(gòu)體,一個是 SOCKADDR_in,還有一個是 SOCKADDR。需要注意的是,這兩個結(jié)構(gòu)體包含的數(shù)據(jù)都是一樣的,是一樣的…
主要是使用上有區(qū)別。有啥區(qū)別?
sockaddr 是個系統(tǒng)用,而 sockaddr 是用來強制轉(zhuǎn)換 sockaddr_in 結(jié)構(gòu)體給系統(tǒng)調(diào)用的函數(shù)用。是不是迷茫?不要迷茫,一般都是這樣做,那就這樣做吧。你只需要記住,sockaddr 保存信息然后就別管了,而sockaddr 咱們就用來給參數(shù)給函數(shù)用。
在 socket 中,咱們使用 sockaddr_in 結(jié)構(gòu)體綁定監(jiān)聽的 IP 信息,首先需要創(chuàng)建這個結(jié)構(gòu)體:
struct sockaddr_in sockAddr;
接下來始綁定端口、IP類型,其中 127.0.0.1 表示本機、1234 表示監(jiān)聽端口:
sockAddr.sin_family = PF_INET; //IPv4sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //服務(wù)器的IPsockAddr.sin_port = htons(1234); //端口
這個懂沒懂?
sockAddr.sin_family 是表示這個結(jié)構(gòu)體中用于存儲IP協(xié)議的結(jié)構(gòu)體變量,PF_INET 之前說了是 ipV4,表示在這里設(shè)置 ipV4類型。
sockAddr.sin_addr.s_addr 這里是表示需要綁定的 ip 地址,在這里使用 inet_addr(“127.0.0.1”) 進行指定。那為什么指定個 ip 還需要 inet_addr?
inet_addr 的作用是將一個字符串格式的ip地址轉(zhuǎn)換成一個uint32_t數(shù)字格式。為什么要轉(zhuǎn)換?那肯定是因為 sockAddr.sin_addr.s_addr 是一個 uint32_t 這個類型了。
最后的 sockAddr.sin_port 是表示要指定某一個端口,在這里指定 1234 這個端口。
所以該部分的代碼就寫成這樣了:
#include<stdio.h>#include<WinSock2.h>#include <stdlib.h>int main(){ WSADATA wsaData; WSAStartup(MAKEWORD(2, 2), &wsadata); SOCKET serverSock = socket(PF_INET, SOCK_STREAM, 0); struct sockaddr_in sockAddr; sockAddr.sin_family = PF_INET; //IPv4 sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //服務(wù)器的IP sockAddr.sin_port = htons(1234); //端口}
最后就是綁定一下了:
bind(serverSock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));
在這里 bind() 方法就是表示綁定信息了,第一個參數(shù)是 serverSock 就是表示要綁定的 socket,然后 (SOCKADDR*)&sockAddr 就是需要綁定的地址,最后一個就是一個地址長度。
(SOCKADDR*)&sockAddr 我們講過,SOCKADDR 就是給函數(shù)使用的,sockAddr 就是給系統(tǒng)使用的,所以就這樣寫就沒毛病了。
2.4 監(jiān)聽端口
先讓你懵一下,下面是代碼:
listen(serverSock, 20);
簡單吧?listen 就是表示監(jiān)聽,第一個參數(shù)就是要監(jiān)聽的 socket 第二個就是表示 同時能處理的最大連接。終于簡單了這一步,你爽我也爽,還不懂就看下面漫畫。
2.5 有人請求聊天?設(shè)置個接待員
接下來就是有人請求給你聊天了,那怎么辦呢?一個人忙不過來呢,那就設(shè)置個接待員。
SOCKADDR cIntAddr; int nSize = sizeof(SOCKADDR);SOCKET cIntSock = accept(serverSock, (SOCKADDR*)&cIntAddr, &nSize);
accept 函數(shù)就是一個接待員,有人連接來敲門了,就需要去接待,換句比較專業(yè)的話就是 accept 接收一個套接字中已建立的連接。
傳入的參數(shù)第一個 serverSock 就是一個已連接的套接字,(SOCKADDR*)&cIntAddr 是一個按照規(guī)定的指向struct sockaddr的指針,所以我猜在前面創(chuàng)建,最后一個就是所指向這個指針的長度咯。
設(shè)置完后就等于創(chuàng)建了一個接待員 cIntSock 。
不過要注意,accept 沒有連接的時候就會一直在等待,不然不會執(zhí)行下面的代碼的。
這一部分的代碼如下:
#include<stdio.h>#include<WinSock2.h>#include <stdlib.h>int main(){ WSADATA wsaData; WSAStartup(MAKEWORD(2, 2), &wsadata); SOCKET serverSock = socket(PF_INET, SOCK_STREAM, 0); struct sockaddr_in sockAddr; sockAddr.sin_family = PF_INET; //IPv4 sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //服務(wù)器的IP sockAddr.sin_port = htons(1234); //端口 listen(serverSock, 20); SOCKADDR cIntAddr; int nSize = sizeof(SOCKADDR); SOCKET cIntSock = accept(serverSock, (SOCKADDR*)&cIntAddr, &nSize);}
2.6 開始循環(huán)聊天
在聊天的時候肯定是需要一個循環(huán),不用循環(huán)只能發(fā)一次信息就完成了,所以肯定有一個 while:
while (1) {}
那循環(huán)里面寫啥?
當然是寫你接收信息和發(fā)送信息的代碼了,我一次性貼上,簡簡單單:
while (1) { char sendBuf[50]={"Hello client"}; char recvBuf[50]; recv(cIntSock, recvBuf, 50, 0); printf("來自客戶端:"); printf("%sn", recvBuf); printf_s("請輸入內(nèi)容:"); scanf("%s",sendBuf); //sendBuf="s"; //gets_s(sendBuf); send(cIntSock, sendBuf, strlen(sendBuf) 1, 0); }
sendBuf就是一個字符數(shù)組,用來輸入自己的要輸入的內(nèi)容。
主要看recv,recv 接收4個參數(shù),第一個參數(shù)是建立的通信、第二個參數(shù)是是一個數(shù)組,接收數(shù)據(jù)存放的地方、之后會緩存大小,最后一個參數(shù)是指定調(diào)用方式,不用管一般設(shè)置為0。
cIntSock 就是剛剛從套接字里接受的那個接待員,現(xiàn)在就用接待員和他說話了。
接著就使用printf顯示接待員聽到的話,簡簡單單。
然后就到我們輸入信息,使用scanf夠簡單了吧?
接著使用 send函數(shù)發(fā)送信息就可以了,第一個就是告訴接待員 cIntSock 要傳達話了,sendBuf 就是咱們要說的話,第三個參數(shù)就是咱們說的話的長度,最后一個依舊是0,不用管。
這樣就還差最后一步就完成服務(wù)端了,此時咱們只需要關(guān)閉套接字就可以了,最后還需要清理一下,完整代碼如下了:
#include<stdio.h>#include<WinSock2.h>#include <stdlib.h>int main(){ WSADATA wsadata; WSAStartup(MAKEWORD(2, 2), &wsadata); SOCKET serverSock = socket(PF_INET, SOCK_STREAM, 0); struct sockaddr_in sockAddr; sockAddr.sin_family = PF_INET; sockAddr.sin_addr.s_addr = htons(INADDR_ANY); sockAddr.sin_port = htons(1234); bind(serverSock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR)); listen(serverSock, 20); SOCKADDR cIntAddr; int nSize = sizeof(SOCKADDR); SOCKET cIntSock = accept(serverSock, (SOCKADDR*)&cIntAddr, &nSize); while (1) { char sendBuf[50]={"Hello client"}; char recvBuf[50]; recv(cIntSock, recvBuf, 50, 0); printf("來自客戶端:"); printf("%sn", recvBuf); printf_s("請輸入內(nèi)容:"); scanf("%s",sendBuf); send(cIntSock, sendBuf, strlen(sendBuf) 1, 0); } //關(guān)閉 closesocket(cIntSock); closesocket(serverSock); WSACleanup(); return 0;}
三、客戶端編寫
客戶端和服務(wù)端是一樣的你信嗎?
下面是代碼:
#include<stdio.h>#include<winsock2.h>int main(){ WSADATA wsadata; int nRes = WSAStartup(MAKEWORD(2, 2), &wsadata); SOCKET sock = socket(PF_INET, SOCK_STREAM, 0); struct sockaddr_in sockAddr; sockAddr.sin_family = PF_INET; sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //只需要在這里指向服務(wù)器 ip 就可以了 sockAddr.sin_port = htons(1234); //連接服務(wù)器 connect(sock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR)); while (1) { char recvBuf[50]; char sendBuf[50]={"Hello server"}; printf("跟服務(wù)端說: "); scanf("%s",sendBuf); send(sock, sendBuf, strlen(sendBuf) 1, 0); recv(sock, recvBuf, 50, 0); printf("服務(wù)端跟你說: "); printf("%sn", recvBuf); } closesocket(sock); WSACleanup(); system("pause");}
不同的幾個點只有使用了 connect 連接服務(wù)器就沒了,難道你說不是嗎?
簡簡單單對吧?那就行,解決。
下面是演示示例:
注意 若使用devc復(fù)制代碼都報錯,則點擊編譯->編譯選項:
隨后在出現(xiàn)的窗口中添加如下參數(shù):
版權(quán)聲明:本文內(nèi)容由互聯(lián)網(wǎng)用戶自發(fā)貢獻,該文觀點僅代表作者本人。本站僅提供信息存儲空間服務(wù),不擁有所有權(quán),不承擔相關(guān)法律責任。如發(fā)現(xiàn)本站有涉嫌抄襲侵權(quán)/違法違規(guī)的內(nèi)容, 請發(fā)送郵件至 舉報,一經(jīng)查實,本站將立刻刪除。