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