C++服務(wù)器和客戶端交互的項目實踐
網(wǎng)絡(luò)與通信Socket
Socket通信三要素:通信的目的地址、使用的端口號(http 80 / smtp 25)、使用的傳輸協(xié)議(TCP、UDP)。
nslookup xx 可以查詢xx網(wǎng)址的IP地址。
Socket通信模型

telnet ipxx 進(jìn)行主機(jī)間通信。
一個簡單的服務(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端口號 唯一標(biāo)識網(wǎng)絡(luò)通訊中的一個進(jìn)程。IP地址+端口號 就對應(yīng)一個socket。與建立連接的兩個進(jìn)程各有一個socket來標(biāo)識,那么這兩個socket組成的socket pair就唯一標(biāo)識一個連接。因此可以用Socket來描述網(wǎng)絡(luò)連接的一對一關(guān)系。

網(wǎng)絡(luò)通訊中,套接字一定是成對出現(xiàn)的。一段的發(fā)送緩沖區(qū)對應(yīng)另一端的接收緩沖區(qū)。使用同一個文件描述符發(fā)送緩沖區(qū)和接收緩沖區(qū)。

服務(wù)器和客戶端之間的通訊是全雙工的,可以互相讀寫,采用同步和異步的方式進(jìn)行交互。
四次揮手結(jié)束客戶端和服務(wù)器端的通訊。
網(wǎng)絡(luò)字節(jié)序
- 大端字節(jié)序-低地址高字節(jié),高地址低字節(jié)。
- 小端字節(jié)序-低地址低字節(jié),高地址高字節(jié)。
內(nèi)存中的多字節(jié)數(shù)據(jù)相對于內(nèi)存地址、磁盤文件中的多字節(jié)數(shù)據(jù)相對于文件中的偏移地址,網(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位長整型,s表示16位短整型。
如果主機(jī)是小端字節(jié)序,這些函數(shù)將參數(shù)做相應(yīng)的大小端轉(zhuǎn)化然后返回,如果主機(jī)是大端字節(jié)序,這些函數(shù)不做轉(zhuǎn)換,將參數(shù)原封不動地返回。
SocketAddr詳解
很多網(wǎng)絡(luò)編程函數(shù)誕生早于IPv4協(xié)議,那時候都使用的是sockaddr結(jié)構(gòu)體,為了向前兼容,現(xiàn)在sockaddr退化成了(void *)的作用,傳遞一個地址給函數(shù),至于這個函數(shù)是sockaddr_in還是其他的,由地址族確定,然后函數(shù)內(nèi)部再強(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位端口號和32位IP地址,但是sock API的實現(xiàn)早于ANSI C標(biāo)準(zhǔn)化,那時還沒有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對應(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é)序為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ù)器在同一臺主機(jī)及同時使用的的協(xié)議。 type: SOCK_STREAM 這個協(xié)議是按照順序的、可靠的、數(shù)據(jù)完成的基于字節(jié)流的連接。這是一個使用最多的socket類型,用于TCP進(jìn)行傳輸?shù)摹? SOCK_DGRAM 這個協(xié)議是無連接的、固定長度的傳輸調(diào)用。該協(xié)議是不可靠的,使用UDP來進(jìn)行連接。 SOCK_SEQPACKET 該協(xié)議是雙線路的,可靠的鏈接,發(fā)送固定長度的數(shù)據(jù)包進(jìn)行傳輸。必須把這個包完整接受才能進(jìn)行讀取。 SOCK_RAW socket類型提供單一的網(wǎng)絡(luò)訪問,這個socket類型使用ICMP公共協(xié)議。(ping、traceroute使用該協(xié)議) SOCK_RDM 這個類型是很少使用的,在大部分操作系統(tǒng)上沒有實現(xiàn),它是提供給數(shù)據(jù)鏈路層使用,不保證數(shù)據(jù)包的順序。 protocol: 傳0表示使用默認(rèn)協(xié)議 return: 成功:返回只想新創(chuàng)建的socket的文件描述符。失?。悍祷?1,并設(shè)置errno。
socket()打開一個網(wǎng)絡(luò)通訊端口,如果成功的話,就像open()一樣返回一個文件描述符,應(yīng)用程序可以向讀寫文件一樣用read/write在網(wǎng)絡(luò)上收發(fā)數(shù)據(jù),如果socket()調(diào)用出錯則返回-1。
bind函數(shù)
服務(wù)器將程序所監(jiān)聽的網(wǎng)絡(luò)地址和端口號通常是固定不變的,客戶端程序得知服務(wù)器程序的地址和端口號就可以向服務(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: 購找出IP地址加端口號 addrlen: sizeof(addr)長度 return: 成功返回0。失敗返回-1,設(shè)置errno。
bind()的作用是將參數(shù)sockfd和addr綁定在一起,使sockfd這個用于網(wǎng)絡(luò)通訊的文件描述符監(jiān)聽addr所描述的地址和端口號。struct sockaddr *是一個通用指針類型,addr參數(shù)實際上可以接受多種協(xié)議的sockaddr結(jié)構(gòu)體,而它們的長度各不相同,所以需要第三個參數(shù)addrlen指定結(jié)構(gòu)體的長度。
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ù)器程序可以同時服務(wù)與多個客戶端,當(dāng)有客戶端發(fā)起連接時,服務(wù)器調(diào)用的accept()函數(shù)返回并接受這個連接,如果有大量的可u段發(fā)起連接而服務(wù)器來不及處理,桑威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)默認(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地址和端口號 addrlen: 傳入傳出參數(shù)(值-結(jié)果),傳入sizeof(addr)大小,函數(shù)返回時返回真正接受到地址結(jié)構(gòu)體的大小 return: 返回一個新的socket文件描述符,用于和客戶端通信,失敗返回-1,并設(shè)置errno
三次握手以后,服務(wù)器調(diào)用accept()接受連接,如果服務(wù)器調(diào)用accept()時還沒有客戶端的連接請求,就阻塞等待直到有客戶端連接上來。addr是一個傳出參數(shù),accept()返回時傳出客戶端的地址和端口號。
服務(wù)器端代碼結(jié)構(gòu)案例:
while (1) {
cliaddr_len = sizeof(cliaddr);
//如果沒有客戶端連接就會一直堵塞在這個代碼上,不會往下進(jìn)行執(zhí)行
connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
n = read(connfd, buf, MAXLINE);
.......
close(connfd);
}整個結(jié)構(gòu)是一個while死循環(huán),每次循環(huán)處理一個客戶端連接,由于cliaddr_len是一個傳入傳出參數(shù),每次調(diào)用accept()之前應(yīng)該重新賦初值。accept()的參數(shù)listenfs是先前監(jiān)聽的文件描述符,而accept()的返回值是另外一個文件描述符connfd,之后與客戶端之間就是通過或者connfd通訊,最后關(guān)閉connfd斷開連接,而不關(guān)閉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ù),指定服務(wù)器端地址信息,含IP地址和端口號 addrlen: 傳入?yún)?shù),指定服務(wù)器段地址信息,含IP地址和端口信息 return: 成功返回0,失敗返回-1,并設(shè)置errno
客戶端需要調(diào)用connect()連接服務(wù)器,connect和bind的參數(shù)一致,區(qū)別在于bind的參數(shù)是自己的地址,而connect的參數(shù)是對方的地址。
出錯處理函數(shù)
系統(tǒng)調(diào)用不能保證每次執(zhí)行都成功,應(yīng)該盡快獲得程序故障信息。
//頭文件 #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: 無 向標(biāo)準(zhǔn)出錯stdeer輸出出錯原因(控制臺打?。?。
到此這篇關(guān)于C++服務(wù)器和客戶端交互的項目實踐的文章就介紹到這了,更多相關(guān)C++服務(wù)器和客戶端交互內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C++下程序運(yùn)行時間的四種常用計時方法總結(jié)
這篇文章主要介紹了C++下程序運(yùn)行時間的四種常用計時方法,介紹了幾種常用的計時方法,包括低精度的clock()和GetTickCount(),以及高精度的gettimeofday()和QueryPerformanceCounter(),需要的朋友可以參考下2024-09-09
UE4 Unlua 調(diào)用異步藍(lán)圖節(jié)點AIMoveTo函數(shù)示例詳解
這篇文章主要為大家介紹了UE4 Unlua 調(diào)用AIMoveTo函數(shù)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09
C語言指針變量作為函數(shù)參數(shù)的實現(xiàn)步驟詳解
這篇文章主要介紹了C語言指針變量作為函數(shù)參數(shù)的實現(xiàn)步驟,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2023-02-02

