简单的socket通信实现
一、什么是socket網(wǎng)絡(luò)通信?
socket也稱作“套接字”,描述了計(jì)算機(jī)的IP地址和端口,運(yùn)行在計(jì)算機(jī)中的程序之間采用socket進(jìn)行數(shù)據(jù)通信。通信的兩端都有socket,它是一個(gè)通道,數(shù)據(jù)在兩個(gè)socket之間進(jìn)行傳輸。
二、套接字(socket)
TCP提供了流(stream)和數(shù)據(jù)報(bào)(datagram)兩種通信機(jī)制,所以套接字也分為流socket和數(shù)據(jù)報(bào)socket。
流socket的類型是SOCK_STREAM,基于TCP協(xié)議,是一個(gè)有序、可靠、雙向字節(jié)流的通道,傳輸數(shù)據(jù)不會(huì)丟失、重復(fù)或亂序到達(dá),而且它還有出錯(cuò)后重傳機(jī)制(就像兩個(gè)人在打電話,聊天您一句我一句,有來(lái)有往,沒(méi)聽(tīng)清楚就再說(shuō)一次)。
數(shù)據(jù)報(bào)套接的類型是SOCK_DGRAM,基于UDP協(xié)議。不需要建立和維持連接,并且對(duì)發(fā)送的數(shù)據(jù)長(zhǎng)度有限制,數(shù)據(jù)報(bào)作為一個(gè)單獨(dú)的網(wǎng)絡(luò)消息被傳輸,它可能會(huì)丟失、重復(fù)或錯(cuò)亂到達(dá),UDP不是一個(gè)可靠的協(xié)議,但是它沒(méi)有擁塞控制,效率比較高。
三、簡(jiǎn)單的socket通信過(guò)程
四、客戶/服務(wù)端模式
1、服務(wù)端流程
1)創(chuàng)建服務(wù)端的socket。
2)把服務(wù)端用于通信的ip地址和端口綁定到socket上。
3)把socket設(shè)置為監(jiān)聽(tīng)模式。
4)接受客戶端的連接。
5)與客戶端通信,接收客戶端發(fā)過(guò)來(lái)的報(bào)文后,回復(fù)處理結(jié)果。
6)不斷的重復(fù)第5)步,直到客戶端斷開(kāi)連接。
7)關(guān)閉socket,釋放資源。
示例(server.cpp)
#include <stdio.h> #include <string.h> #include <unistd.h> #include <netdb.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h>int main() {// 第1步:創(chuàng)建服務(wù)端的socket。int listenfd = socket(AF_INET,SOCK_STREAM,0); // 第2步:把服務(wù)端用于通信的地址和端口綁定到socket上。struct sockaddr_in servaddr; // 服務(wù)端地址信息的數(shù)據(jù)結(jié)構(gòu)。memset(&servaddr,0,sizeof(servaddr));servaddr.sin_family = AF_INET; // 協(xié)議族,在socket編程中只能是AF_INET。servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 任意ip地址。//servaddr.sin_addr.s_addr = inet_addr("118.89.50.198"); // 指定ip地址。servaddr.sin_port = htons(5051); // 指定通信端口。if (bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)) != 0 ){ perror("bind"); close(listenfd); return -1; }// 第3步:把socket設(shè)置為監(jiān)聽(tīng)模式。if (listen(listenfd,5) != 0 ) { perror("listen"); close(listenfd); return -1; }// 第4步:接受客戶端的連接。int clientfd; // 客戶端的socket。int socklen=sizeof(struct sockaddr_in); // struct sockaddr_in的大小struct sockaddr_in clientaddr; // 客戶端的地址信息。clientfd=accept(listenfd,(struct sockaddr *)&clientaddr,(socklen_t*)&socklen);printf("客戶端(%s)已連接。\n",inet_ntoa(clientaddr.sin_addr));// 第5步:與客戶端通信,接收客戶端發(fā)過(guò)來(lái)的報(bào)文后,回復(fù)ok。char buffer[1024];while (1){memset(buffer,0,sizeof(buffer));if (recv(clientfd,buffer,sizeof(buffer),0)<=0) break; // 接收客戶端的請(qǐng)求報(bào)文。printf("接收:%s\n",buffer);strcpy(buffer,"ok");if (send(clientfd,buffer,strlen(buffer),0)<=0) break; // 向客戶端發(fā)送響應(yīng)結(jié)果。printf("發(fā)送:%s\n",buffer);}// 第6步:關(guān)閉socket,釋放資源。close(listenfd); close(clientfd); }2、客戶端流程
1)創(chuàng)建客戶端的socket。
2)向服務(wù)器發(fā)起連接請(qǐng)求。
3)與服務(wù)端通信,發(fā)送一個(gè)報(bào)文后等待回復(fù),然后再發(fā)下一個(gè)報(bào)文。
4)不斷的重復(fù)第3)步,直到全部的數(shù)據(jù)被發(fā)送完。
5)第4步:關(guān)閉socket,釋放資源。
示例(client.cpp)
#include <stdio.h> #include <string.h> #include <unistd.h> #include <netdb.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h>int main() {// 第1步:創(chuàng)建客戶端的socket。int sockfd = socket(AF_INET,SOCK_STREAM,0); // 第2步:向服務(wù)器發(fā)起連接請(qǐng)求。struct hostent* h; if ( (h = gethostbyname("118.89.50.198")) == 0 ) // 指定服務(wù)端的ip地址。{ perror("gethostbyname"); close(sockfd); return -1; }struct sockaddr_in servaddr;memset(&servaddr,0,sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_port = htons(5051); // 指定服務(wù)端的通信端口。memcpy(&servaddr.sin_addr,h->h_addr,h->h_length);if (connect(sockfd, (struct sockaddr *)&servaddr,sizeof(servaddr)) != 0) // 向服務(wù)端發(fā)起連接清求。{ perror("connect"); close(sockfd); return -1; }char buffer[1024];// 第3步:與服務(wù)端通信,發(fā)送一個(gè)報(bào)文后等待回復(fù),然后再發(fā)下一個(gè)報(bào)文。for (int ii=0;ii<3;ii++){memset(buffer,0,sizeof(buffer));sprintf(buffer,"這是第%d個(gè)超級(jí)女生,編號(hào)%03d。",ii+1,ii+1);if (send(sockfd,buffer,strlen(buffer),0)<=0) break; // 向服務(wù)端發(fā)送請(qǐng)求報(bào)文。printf("發(fā)送:%s\n",buffer);memset(buffer,0,sizeof(buffer));if (recv(sockfd,buffer,sizeof(buffer),0)<=0) break; // 接收服務(wù)端返回的結(jié)果。printf("接收:%s\n",buffer);}// 第4步:關(guān)閉socket,釋放資源。close(sockfd); }在運(yùn)行程序之前,必須保證服務(wù)器的防火墻已經(jīng)開(kāi)通了網(wǎng)絡(luò)訪問(wèn)策略
五、注解
1、 說(shuō)明
在socket通信的客戶端和服務(wù)器的程序里,有多種數(shù)據(jù)結(jié)構(gòu),調(diào)用了多個(gè)函數(shù),涉及到很多方面的知識(shí),這篇文章主要是了解socket通信的過(guò)程、每段代碼的用途和函數(shù)調(diào)用的功能,對(duì)于一些復(fù)雜的結(jié)構(gòu)體和函數(shù)的參數(shù),我會(huì)在另一篇文章詳細(xì)講解。
2、服務(wù)端程序綁定地址
如果服務(wù)器有多個(gè)網(wǎng)卡,多個(gè)IP地址,socket通信可以指定用其中一個(gè)地址來(lái)進(jìn)行通信,也可以任意ip地址。
1)指定ip地址的代碼
m_servaddr.sin_addr.s_addr = inet_addr("192.168.149.129"); // 指定ip地址2)任意ip地址的代碼
m_servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 本主機(jī)的任意ip地址在實(shí)際開(kāi)發(fā)中,采用任意ip地址的方式比較多。
3、服務(wù)端程序綁定的通信端口
m_servaddr.sin_port = htons(5000); // 通信端口4、客戶端程序指定服務(wù)端的ip地址
struct hostent* h; // ip地址信息的數(shù)據(jù)結(jié)構(gòu) if ( (h = gethostbyname("192.168.149.129")) == 0 ) { perror("gethostbyname"); close(sockfd); return -1; }5、客戶端程序指定服務(wù)端的通信端口
servaddr.sin_port = htons(5000);6、send函數(shù)
send函數(shù)用于把數(shù)據(jù)通過(guò)socket發(fā)送給對(duì)端。不論是客戶端還是服務(wù)端,應(yīng)用程序都用send函數(shù)來(lái)向TCP連接的另一端發(fā)送數(shù)據(jù)。
函數(shù)聲明:
ssize_t send(int sockfd, const void *buf, size_t len, int flags);sockfd為已建立好連接的socket。
buf為需要發(fā)送的數(shù)據(jù)的內(nèi)存地址,可以是C語(yǔ)言基本數(shù)據(jù)類型變量的地址,也可以數(shù)組、結(jié)構(gòu)體、字符串,內(nèi)存中有什么就發(fā)送什么。
len需要發(fā)送的數(shù)據(jù)的長(zhǎng)度,為buf中有效數(shù)據(jù)的長(zhǎng)度。
flags填0, 其他數(shù)值意義不大。
函數(shù)返回已發(fā)送的字符數(shù)。出錯(cuò)時(shí)返回-1,錯(cuò)誤信息errno被標(biāo)記。
注意,就算是網(wǎng)絡(luò)斷開(kāi),或socket已被對(duì)端關(guān)閉,send函數(shù)不會(huì)立即報(bào)錯(cuò),要過(guò)幾秒才會(huì)報(bào)錯(cuò)。
如果send函數(shù)返回的錯(cuò)誤(<=0),表示通信鏈路已不可用。
7、recv函數(shù)
recv函數(shù)用于接收對(duì)端socket發(fā)送過(guò)來(lái)的數(shù)據(jù)。
recv函數(shù)用于接收對(duì)端通過(guò)socket發(fā)送過(guò)來(lái)的數(shù)據(jù)。不論是客戶端還是服務(wù)端,應(yīng)用程序都用recv函數(shù)接收來(lái)自TCP連接的另一端發(fā)送過(guò)來(lái)數(shù)據(jù)。
函數(shù)聲明:
ssize_t recv(int sockfd, void *buf, size_t len, int flags);sockfd為已建立好連接的socket。
buf為用于接收數(shù)據(jù)的內(nèi)存地址,可以是C語(yǔ)言基本數(shù)據(jù)類型變量的地址,也可以數(shù)組、結(jié)構(gòu)體、字符串,只要是一塊內(nèi)存就行了。
len需要接收數(shù)據(jù)的長(zhǎng)度,不能超過(guò)buf的大小,否則內(nèi)存溢出。
flags填0, 其他數(shù)值意義不大。
如果socket的對(duì)端沒(méi)有發(fā)送數(shù)據(jù),recv函數(shù)就會(huì)等待,如果對(duì)端發(fā)送了數(shù)據(jù),函數(shù)返回接收到的字符數(shù)。出錯(cuò)時(shí)返回-1,錯(cuò)誤信息errno被標(biāo)記。如果socket被對(duì)端關(guān)閉,返回值為0。
如果recv函數(shù)返回的錯(cuò)誤(<=0),表示通信通道已不可用。
8、服務(wù)端有兩個(gè)socket
對(duì)服務(wù)端來(lái)說(shuō),有兩個(gè)socket,一個(gè)是用于監(jiān)聽(tīng)的socket,還有一個(gè)就是客戶端連接成功后,由accept函數(shù)創(chuàng)建的用于與客戶端收發(fā)報(bào)文的socket。
9、程序退出時(shí)先關(guān)閉socket
socket是系統(tǒng)資源,操作系統(tǒng)打開(kāi)的socket數(shù)量是有限的(默認(rèn)是1024,具體可以通過(guò)ulimit -a命令查看),在程序退出之前必須關(guān)閉已打開(kāi)的socket,就像關(guān)閉文件指針一樣,就像delete已分配的內(nèi)存一樣,極其重要。
值得注意的是,關(guān)閉socket的代碼不能只在main函數(shù)的最后,那是程序運(yùn)行的理想狀態(tài),還應(yīng)該在main函數(shù)的每個(gè)return之前關(guān)閉。
六、用到的庫(kù)函數(shù)
1、socket()
socket函數(shù)用于創(chuàng)建一個(gè)新的socket,也就是向系統(tǒng)申請(qǐng)一個(gè)socket資源。socket函數(shù)用戶客戶端和服務(wù)端。
函數(shù)聲明:
int socket(int domain, int type, int protocol);參數(shù)說(shuō)明:
domain:協(xié)議域,又稱協(xié)議族(family)。常用的協(xié)議族有AF_INET、AF_INET6、AF_LOCAL(或稱AF_UNIX,Unix域Socket)、AF_ROUTE等。協(xié)議族決定了socket的地址類型,在通信中必須采用對(duì)應(yīng)的地址,如AF_INET決定了要用ipv4地址(32位的)與端口號(hào)(16位的)的組合、AF_UNIX決定了要用一個(gè)絕對(duì)路徑名作為地址。
type:指定socket類型。常用的socket類型有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等。流式socket(SOCK_STREAM)是一種面向連接的socket,針對(duì)于面向連接的TCP服務(wù)應(yīng)用。數(shù)據(jù)報(bào)式socket(SOCK_DGRAM)是一種無(wú)連接的socket,對(duì)應(yīng)于無(wú)連接的UDP服務(wù)應(yīng)用。
protocol:指定協(xié)議。常用協(xié)議有IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP、IPPROTO_TIPC等,分別對(duì)應(yīng)TCP傳輸協(xié)議、UDP傳輸協(xié)議、STCP傳輸協(xié)議、TIPC傳輸協(xié)議。
第一個(gè)參數(shù)填A(yù)F_INET,第二個(gè)參數(shù)只能填SOCK_STREAM,第三個(gè)參數(shù)只能填0。
除非系統(tǒng)資源耗盡,socket函數(shù)一般不會(huì)返回失敗。
2、gethostbyname()
把ip地址或域名轉(zhuǎn)換為hostent 結(jié)構(gòu)體表達(dá)的地址。
函數(shù)聲明:
struct hostent *gethostbyname(const char *name);參數(shù)name,域名或者主機(jī)名,例如"192.168.1.3"、"www.baidu.com"等。
返回值:如果成功,返回一個(gè)hostent結(jié)構(gòu)指針,失敗返回NULL。
gethostbyname只用于客戶端。
gethostbyname只是把字符串的ip地址轉(zhuǎn)換為結(jié)構(gòu)體的ip地址,只要地址格式?jīng)]錯(cuò),一般不會(huì)返回錯(cuò)誤。函數(shù)失敗不會(huì)設(shè)置errno的值。
3、connect()
向服務(wù)器發(fā)起連接請(qǐng)求。
函數(shù)聲明:
int connect(int sockfd, struct sockaddr * serv_addr, int addrlen);函數(shù)說(shuō)明:connect函數(shù)用于將參數(shù)sockfd 的socket 連至參數(shù)serv_addr
指定的服務(wù)端,參數(shù)addrlen為sockaddr的結(jié)構(gòu)長(zhǎng)度。
返回值:成功則返回0, 失敗返回-1, 錯(cuò)誤原因存于errno 中。
connect函數(shù)只用于客戶端。
如果服務(wù)端的地址錯(cuò)了,或端口錯(cuò)了,或服務(wù)端沒(méi)有啟動(dòng),connect一定會(huì)失敗。
4、bind()
服務(wù)端把用于通信的地址和端口綁定到socket上。
函數(shù)聲明:
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);參數(shù)sockfd,需要綁定的socket。
參數(shù)addr,存放了服務(wù)端用于通信的地址和端口。
參數(shù)addrlen表示addr結(jié)構(gòu)體的大小。
如果綁定的地址錯(cuò)誤,或端口已被占用,bind函數(shù)一定會(huì)報(bào)錯(cuò),否則一般不會(huì)返回錯(cuò)誤。
5、listen()
listen函數(shù)把主動(dòng)連接套接字變?yōu)楸粍?dòng)連接的套接字,使得這個(gè)socket可以接受其它socket的連接請(qǐng)求,從而成為一個(gè)服務(wù)端的socket。
函數(shù)聲明:
int listen(int sockfd, int backlog);返回:0-成功, -1-失敗
sockfd:已經(jīng)被bind過(guò)的套接字。socket函數(shù)返回的套接字是一個(gè)主動(dòng)連接的套接字,在服務(wù)端的編程中,程序員希望這個(gè)套接字可以接受外來(lái)的連接請(qǐng)求,也就是被動(dòng)等待客戶端來(lái)連接。由于系統(tǒng)默認(rèn)時(shí)認(rèn)為一個(gè)套接字是主動(dòng)連接的,所以需要通過(guò)某種方式來(lái)告訴系統(tǒng),程序員通過(guò)調(diào)用listen函數(shù)來(lái)完成這件事。
backlog:這個(gè)參數(shù)涉及到一些網(wǎng)絡(luò)的細(xì)節(jié),填5、10都行,一般不超過(guò)30。
當(dāng)調(diào)用listen之后,服務(wù)端的套接字就可以調(diào)用accept來(lái)接受客戶端的連接請(qǐng)求。
listen函數(shù)一般不會(huì)返回錯(cuò)誤。
6、accept()
服務(wù)端接受客戶端的連接。
函數(shù)聲明:
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);sockfd:已經(jīng)被listen過(guò)的套接字。
addr: 用于存放客戶端的地址信息,用sockaddr結(jié)構(gòu)體表達(dá),如果不需要客戶端的地址,可以填0。
addrlen:用于存放addr參數(shù)的長(zhǎng)度,如果addr為0,addrlen也填0。
accept:函數(shù)等待客戶端的連接,如果沒(méi)有客戶端連上來(lái),它就一直等待,這種方式稱之為阻塞。
accept等待到客戶端的連接后,創(chuàng)建一個(gè)新的套接字,函數(shù)返回值就是這個(gè)新的套接字,服務(wù)端使用這個(gè)新的套接字和客戶端進(jìn)行報(bào)文的收發(fā)。
accept在等待的過(guò)程中,如果被中斷或其它的原因,函數(shù)返回-1,表示失敗,如果失敗,可以重新accept。
七:注意事項(xiàng)
listen()、connect0和accept()函數(shù)
| 2)服務(wù)端調(diào)用listen()函數(shù)后,服務(wù)端的socket開(kāi)始監(jiān)聽(tīng)客戶端的連接。 |
| 3)客戶端調(diào)用connect()函數(shù)向服務(wù)端發(fā)起連接請(qǐng)求。 |
| 4)在TCP底層,客戶端和服務(wù)端握手后建立起通信通道,如果有多個(gè)客戶端請(qǐng)求,在服務(wù)端就會(huì)形成一個(gè)已準(zhǔn)備好的連接隊(duì)列。 |
| 5)服務(wù)端調(diào)用accept()函數(shù)從隊(duì)列中獲取一個(gè)已準(zhǔn)備好的連接,函數(shù)返回一個(gè)新的socket, 新的socket用于與客戶端通信,listen的socket只負(fù)責(zé)監(jiān)聽(tīng)客戶端的連接請(qǐng)求。 |
本文參考c語(yǔ)言技術(shù)網(wǎng)的文章加以總結(jié)。
總結(jié)
以上是生活随笔為你收集整理的简单的socket通信实现的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 实现库函数strcpy
- 下一篇: socket通信中的几个重要结构体定义