C++服務(wù)器和客戶端交互的項(xiàng)目實(shí)踐
網(wǎng)絡(luò)與通信Socket
Socket通信三要素:通信的目的地址、使用的端口號(hào)(http 80 / smtp 25)、使用的傳輸協(xié)議(TCP、UDP)。
nslookup xx
可以查詢xx網(wǎng)址的IP地址。
Socket通信模型
telnet ipxx
進(jìn)行主機(jī)間通信。
一個(gè)簡(jiǎn)單的服務(wù)器和客戶端通信程序,服務(wù)器端代碼:
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <ctype.h> #include <arpa/inet.h> #define SERVER_PORT 666 int main(void) { int sock; struct sockaddr_in server_addr; sock = socket(AF_INET, SOCK_STREAM, 0); // printf("wait \n"); bzero(&server_addr, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = htonl(INADDR_ANY); server_addr.sin_port = htons(SERVER_PORT); bind(sock, (struct sockaddr *)&server_addr, sizeof(server_addr)); listen(sock, 128); printf("wait client connect\n"); int done = 1; while (done) { struct sockaddr_in client; int client_sock, len, i; char client_ip[64]; char buf[256]; socklen_t client_addr_len; client_addr_len = sizeof(client); client_sock = accept(sock, (struct sockaddr *)&client, &client_addr_len); printf("client ip: %s \t port is : %d \n", inet_ntop(AF_INET, &client.sin_addr.s_addr, client_ip, sizeof(client_ip)), ntohs(client.sin_port)); len = read(client_sock, buf, sizeof(buf) - 1); buf[len] = '\0'; printf("receive[%d]:%s\n", len, buf); len = write(client_sock, buf, len); printf("finish. len:%d\n", len); close(client_sock); } close(sock); return 0; }
客戶端代碼:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #define SERVER_PORT 666 #define SERVER_IP "127.0.0.1" int main(int argc, char *argv[]){ int sockfd; char *message; struct sockaddr_in servaddr; int n; char buf[64]; if(argc != 2){ fputs("Usage: ./echo_client message \n", stderr); exit(1); } message = argv[1]; printf("message: %s\n", message); sockfd = socket(AF_INET, SOCK_STREAM, 0); //重置結(jié)構(gòu)體的內(nèi)存空間 memset(&servaddr, '\0', sizeof(struct sockaddr_in)); servaddr.sin_family = AF_INET; inet_pton(AF_INET, SERVER_IP, &servaddr.sin_addr); servaddr.sin_port = htons(SERVER_PORT); connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); write(sockfd, message, strlen(message)); n = read(sockfd, buf, sizeof(buf)-1); if(n>0){ buf[n]='\0'; printf("receive: %s\n", buf); }else { perror("error!!!"); } printf("finished.\n"); close(sockfd); return 0; }
Socket概念
socket(套接字)的中文意思為插座,socket一般用整型表示,Linux中,表示進(jìn)程x間網(wǎng)絡(luò)通信的特殊文件類型。本質(zhì)上為內(nèi)核借助緩沖區(qū)形成的為文件。可以使用文件描述符引用套接字。Linux系統(tǒng)將其封裝成文件的目的是為了統(tǒng)一接口,使讀寫套接字和讀寫文件操作一致。區(qū)別在于文件主要因用于本地持久化數(shù)據(jù)的讀寫,而套接字多應(yīng)用于網(wǎng)絡(luò)進(jìn)程間數(shù)據(jù)的傳遞。
在TCP/IP協(xié)議中,IP地址-TCP或UDP端口號(hào) 唯一標(biāo)識(shí)網(wǎng)絡(luò)通訊中的一個(gè)進(jìn)程。IP地址+端口號(hào) 就對(duì)應(yīng)一個(gè)socket。與建立連接的兩個(gè)進(jìn)程各有一個(gè)socket來標(biāo)識(shí),那么這兩個(gè)socket組成的socket pair就唯一標(biāo)識(shí)一個(gè)連接。因此可以用Socket來描述網(wǎng)絡(luò)連接的一對(duì)一關(guān)系。
網(wǎng)絡(luò)通訊中,套接字一定是成對(duì)出現(xiàn)的。一段的發(fā)送緩沖區(qū)對(duì)應(yīng)另一端的接收緩沖區(qū)。使用同一個(gè)文件描述符發(fā)送緩沖區(qū)和接收緩沖區(qū)。
服務(wù)器和客戶端之間的通訊是全雙工的,可以互相讀寫,采用同步和異步的方式進(jìn)行交互。
四次揮手結(jié)束客戶端和服務(wù)器端的通訊。
網(wǎng)絡(luò)字節(jié)序
- 大端字節(jié)序-低地址高字節(jié),高地址低字節(jié)。
- 小端字節(jié)序-低地址低字節(jié),高地址高字節(jié)。
內(nèi)存中的多字節(jié)數(shù)據(jù)相對(duì)于內(nèi)存地址、磁盤文件中的多字節(jié)數(shù)據(jù)相對(duì)于文件中的偏移地址,網(wǎng)絡(luò)數(shù)據(jù)流都有大端和小端之分。發(fā)送主機(jī)通常將發(fā)送緩沖區(qū)中的數(shù)據(jù)按內(nèi)存地址從低到高的順序發(fā)出,接收主機(jī)把從網(wǎng)絡(luò)上接到的字節(jié)一次保存在接受緩沖區(qū)中,也是按照內(nèi)存地址從低到高的順序保存。因此,網(wǎng)絡(luò)數(shù)據(jù)流的地址應(yīng)該這樣規(guī)定:先發(fā)出的數(shù)據(jù)是低地址,后發(fā)出的數(shù)據(jù)是高地址。
TCP/IP協(xié)議規(guī)定,網(wǎng)絡(luò)數(shù)據(jù)流應(yīng)采用大端字節(jié)序,既低地址高字節(jié)。
32位IP地址也要考慮網(wǎng)絡(luò)字節(jié)序和主機(jī)字節(jié)序的問題。C/C++中采用一下庫函數(shù)進(jìn)行網(wǎng)絡(luò)字節(jié)序和主機(jī)字節(jié)序的轉(zhuǎn)換。
//頭文件,庫函數(shù) #include<arpa/inet.h> uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort);
h
表示host
,n
表示network
,l
表示32位長(zhǎng)整型,s
表示16位短整型。
如果主機(jī)是小端字節(jié)序,這些函數(shù)將參數(shù)做相應(yīng)的大小端轉(zhuǎn)化然后返回,如果主機(jī)是大端字節(jié)序,這些函數(shù)不做轉(zhuǎn)換,將參數(shù)原封不動(dòng)地返回。
SocketAddr詳解
很多網(wǎng)絡(luò)編程函數(shù)誕生早于IPv4協(xié)議,那時(shí)候都使用的是sockaddr
結(jié)構(gòu)體,為了向前兼容,現(xiàn)在sockaddr
退化成了(void *)
的作用,傳遞一個(gè)地址給函數(shù),至于這個(gè)函數(shù)是sockaddr_in
還是其他的,由地址族確定,然后函數(shù)內(nèi)部再?gòu)?qiáng)制類型轉(zhuǎn)化為所需的地址類型。
SocketAddress結(jié)構(gòu)圖
struct sockaddr { sa_family_t sa_family;/* address family, AF_xxx AF_INET(IPV4) AF_INET(IPV6)*/ char sa_data[14]; /* 14 bytes of protocol address */ }; struct sockaddr_in { sa_family_t sin_family; /* address family: AF_INET */ in_port_t sin_port; /* port in network byte order */ struct in_addr sin_addr; /* internet address */ }; /* Internet address. */ struct in_addr { uint32_t s_addr; /* address in network byte order */ };
IPv4的地址格式定義在netinet/in.h
中,IPv4地址用sockaddr_in
結(jié)構(gòu)體表示,包括16位端口號(hào)和32位IP地址,但是sock API的實(shí)現(xiàn)早于ANSI C
標(biāo)準(zhǔn)化,那時(shí)還沒有void *
類型,因此這些像bind
、accept
函數(shù)的參數(shù)都用struct sockaddr *
類型表示,在傳遞參數(shù)之前要強(qiáng)制類型轉(zhuǎn)換一下,例如:
struct sockaddr_in servaddr; bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));/* initialize servaddr */
IP地址轉(zhuǎn)化
#include <arpa/inet.h> //將字符串的IP轉(zhuǎn)化為網(wǎng)絡(luò)的整型IP int inet_pton(int af, const char *src, void *dst); //將網(wǎng)絡(luò)字節(jié)序的IP轉(zhuǎn)化為字符串的IP const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
af
取值可選為 AF_INET
和 AF_INET6
,即和 ipv4 和ipv6對(duì)應(yīng)支持IPv4和IPv6假設(shè)主機(jī)地址位2.3.4.5
,其中2
表示低位,5
表示高位,則大端字節(jié)序的結(jié)果為5040302
(使用inet_pton(AF_INET,"2.3.4.5",&s_add)
進(jìn)行轉(zhuǎn)化),小端字節(jié)序?yàn)?code>2030405(使用ntohl(s_addr)
進(jìn)行轉(zhuǎn)化)。
ipconfig /all
查看主機(jī)的網(wǎng)絡(luò)地址。
Socket編程
socket函數(shù)
//頭文件 #include<sys/types.h> #include<sys/socket.h> int socket(int doamin,int type,int protocol); domain: AF_INET 這是大多數(shù)用來產(chǎn)生socket的協(xié)議,使用TCP或UDP來傳輸,用IPV4的地址。 AF_INET6 和AF_INET類似,不過是用來IPV6的地址。 AF_UNIX 本地協(xié)議,使用在Unix和Linux系統(tǒng)上,一般都是當(dāng)客戶端和服務(wù)器在同一臺(tái)主機(jī)及同時(shí)使用的的協(xié)議。 type: SOCK_STREAM 這個(gè)協(xié)議是按照順序的、可靠的、數(shù)據(jù)完成的基于字節(jié)流的連接。這是一個(gè)使用最多的socket類型,用于TCP進(jìn)行傳輸?shù)摹? SOCK_DGRAM 這個(gè)協(xié)議是無連接的、固定長(zhǎng)度的傳輸調(diào)用。該協(xié)議是不可靠的,使用UDP來進(jìn)行連接。 SOCK_SEQPACKET 該協(xié)議是雙線路的,可靠的鏈接,發(fā)送固定長(zhǎng)度的數(shù)據(jù)包進(jìn)行傳輸。必須把這個(gè)包完整接受才能進(jìn)行讀取。 SOCK_RAW socket類型提供單一的網(wǎng)絡(luò)訪問,這個(gè)socket類型使用ICMP公共協(xié)議。(ping、traceroute使用該協(xié)議) SOCK_RDM 這個(gè)類型是很少使用的,在大部分操作系統(tǒng)上沒有實(shí)現(xiàn),它是提供給數(shù)據(jù)鏈路層使用,不保證數(shù)據(jù)包的順序。 protocol: 傳0表示使用默認(rèn)協(xié)議 return: 成功:返回只想新創(chuàng)建的socket的文件描述符。失?。悍祷?1,并設(shè)置errno。
socket()
打開一個(gè)網(wǎng)絡(luò)通訊端口,如果成功的話,就像open()
一樣返回一個(gè)文件描述符,應(yīng)用程序可以向讀寫文件一樣用read/write
在網(wǎng)絡(luò)上收發(fā)數(shù)據(jù),如果socket()
調(diào)用出錯(cuò)則返回-1。
bind函數(shù)
服務(wù)器將程序所監(jiān)聽的網(wǎng)絡(luò)地址和端口號(hào)通常是固定不變的,客戶端程序得知服務(wù)器程序的地址和端口號(hào)就可以向服務(wù)器發(fā)起連接,因此服務(wù)器需要調(diào)用bind
進(jìn)行綁定。
//頭文件 #include<sys/type.h> #include<sys/socket.h> int bind(int sockfd,const struct sockaddr *addr,socklen_t addren); sockfd: socket文件描述符 addr: 購(gòu)找出IP地址加端口號(hào) addrlen: sizeof(addr)長(zhǎng)度 return: 成功返回0。失敗返回-1,設(shè)置errno。
bind()
的作用是將參數(shù)sockfd
和addr
綁定在一起,使sockfd
這個(gè)用于網(wǎng)絡(luò)通訊的文件描述符監(jiān)聽addr
所描述的地址和端口號(hào)。struct sockaddr *
是一個(gè)通用指針類型,addr
參數(shù)實(shí)際上可以接受多種協(xié)議的sockaddr
結(jié)構(gòu)體,而它們的長(zhǎng)度各不相同,所以需要第三個(gè)參數(shù)addrlen
指定結(jié)構(gòu)體的長(zhǎng)度。
struct sockaddr_in servaddr; //結(jié)構(gòu)體清空很重要 bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(6666);
Listen函數(shù)
典型的服務(wù)器程序可以同時(shí)服務(wù)與多個(gè)客戶端,當(dāng)有客戶端發(fā)起連接時(shí),服務(wù)器調(diào)用的accept()
函數(shù)返回并接受這個(gè)連接,如果有大量的可u段發(fā)起連接而服務(wù)器來不及處理,桑威accept的客戶端就處于這個(gè)連接等待狀態(tài),listen()
僧名sockfd處于監(jiān)聽狀態(tài),如果接受到更多的連接請(qǐng)求就忽略,listen()
成功返回0,失敗返回-1.
//頭文件 #include<sys/types.h> #include<sys/socket.h> int listen(int sockfd,int backlog); sockfd: socket文件描述符 backlog: 在Linux系統(tǒng)中,它是指排隊(duì)等待建立3次握手隊(duì)列長(zhǎng)度
查看系統(tǒng)默認(rèn)backlog
cat /proc/sys/net/ipv4/tcp_max_syn_backlog
accept函數(shù)
//頭文件 #include<sys/types.h> #include<sys/socket.h> int accept(int sockfd,struct aockaddr *addr,socklen_t *addrlen); sockdf: socket文件描述符 addr: 傳出的參數(shù),返回鏈接客戶端地址信息,含IP地址和端口號(hào) addrlen: 傳入傳出參數(shù)(值-結(jié)果),傳入sizeof(addr)大小,函數(shù)返回時(shí)返回真正接受到地址結(jié)構(gòu)體的大小 return: 返回一個(gè)新的socket文件描述符,用于和客戶端通信,失敗返回-1,并設(shè)置errno
三次握手以后,服務(wù)器調(diào)用accept()
接受連接,如果服務(wù)器調(diào)用accept()
時(shí)還沒有客戶端的連接請(qǐng)求,就阻塞等待直到有客戶端連接上來。addr
是一個(gè)傳出參數(shù),accept()
返回時(shí)傳出客戶端的地址和端口號(hào)。
服務(wù)器端代碼結(jié)構(gòu)案例:
while (1) { cliaddr_len = sizeof(cliaddr); //如果沒有客戶端連接就會(huì)一直堵塞在這個(gè)代碼上,不會(huì)往下進(jìn)行執(zhí)行 connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len); n = read(connfd, buf, MAXLINE); ....... close(connfd); }
整個(gè)結(jié)構(gòu)是一個(gè)while
死循環(huán),每次循環(huán)處理一個(gè)客戶端連接,由于cliaddr_len
是一個(gè)傳入傳出參數(shù),每次調(diào)用accept()
之前應(yīng)該重新賦初值。accept()
的參數(shù)listenfs
是先前監(jiān)聽的文件描述符,而accept()
的返回值是另外一個(gè)文件描述符connfd
,之后與客戶端之間就是通過或者connfd
通訊,最后關(guān)閉connfd
斷開連接,而不關(guān)閉listenfd
,再次回到循環(huán)開頭listenfd
仍然用作accept
參數(shù)。accept()
成功返回一個(gè)文件描述符,出錯(cuò)返回-1。
connect函數(shù)
//頭文件 #include<sys/types.h> #include<sys/socket.h> int connect(int sockfd,const struct sockaddr *addr,socklen_t addrlen); sockdf: socket文件描述符 addr: 傳入?yún)?shù),指定服務(wù)器端地址信息,含IP地址和端口號(hào) addrlen: 傳入?yún)?shù),指定服務(wù)器段地址信息,含IP地址和端口信息 return: 成功返回0,失敗返回-1,并設(shè)置errno
客戶端需要調(diào)用connect()
連接服務(wù)器,connect
和bind
的參數(shù)一致,區(qū)別在于bind
的參數(shù)是自己的地址,而connect
的參數(shù)是對(duì)方的地址。
出錯(cuò)處理函數(shù)
系統(tǒng)調(diào)用不能保證每次執(zhí)行都成功,應(yīng)該盡快獲得程序故障信息。
//頭文件 #include<errno.h> #include<string.h> char *strerror(int errnum) errnum: 傳入?yún)?shù),錯(cuò)誤編號(hào)的值,一般去errno的值 return: 錯(cuò)誤原因 #include<stdio.h> #include<errno.h> void perror(const char *s); s: 傳入?yún)?shù),自定義描述 return: 無 向標(biāo)準(zhǔn)出錯(cuò)stdeer輸出出錯(cuò)原因(控制臺(tái)打?。?/pre>
到此這篇關(guān)于C++服務(wù)器和客戶端交互的項(xiàng)目實(shí)踐的文章就介紹到這了,更多相關(guān)C++服務(wù)器和客戶端交互內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C++實(shí)現(xiàn)獲取時(shí)間戳和計(jì)算運(yùn)行時(shí)長(zhǎng)
這篇文章主要為大家詳細(xì)介紹了如何使用C++實(shí)現(xiàn)獲取時(shí)間戳和計(jì)算運(yùn)行時(shí)長(zhǎng)功能,文中的示例代碼講解詳細(xì),有需要的小伙伴可以參考一下2024-12-12C++下程序運(yùn)行時(shí)間的四種常用計(jì)時(shí)方法總結(jié)
這篇文章主要介紹了C++下程序運(yùn)行時(shí)間的四種常用計(jì)時(shí)方法,介紹了幾種常用的計(jì)時(shí)方法,包括低精度的clock()和GetTickCount(),以及高精度的gettimeofday()和QueryPerformanceCounter(),需要的朋友可以參考下2024-09-09UE4 Unlua 調(diào)用異步藍(lán)圖節(jié)點(diǎn)AIMoveTo函數(shù)示例詳解
這篇文章主要為大家介紹了UE4 Unlua 調(diào)用AIMoveTo函數(shù)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09C語言指針變量作為函數(shù)參數(shù)的實(shí)現(xiàn)步驟詳解
這篇文章主要介紹了C語言指針變量作為函數(shù)參數(shù)的實(shí)現(xiàn)步驟,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2023-02-02