C/C++?Linux?Socket網(wǎng)絡(luò)編程流程分析
之前已經(jīng)學(xué)習(xí)了QT的socket編程 和 C/C++在window環(huán)境的socket編程,現(xiàn)在再來(lái)學(xué)習(xí)一波C/C++在Linux環(huán)境下的socket編程,為以后學(xué)習(xí)C++ Linux 服務(wù)器開(kāi)發(fā)做準(zhǔn)備。
一、Socket簡(jiǎn)介
既然是socket,那必然有TCP 和 UDP之分,本文所記錄的是TCP協(xié)議的socket編程。
socket編程分為T(mén)CP和UDP兩個(gè)模塊,其中TCP是可靠的、安全的,常用于發(fā)送文件等,而UDP是不可靠的、不安全的,常用作視頻通話(huà)等。
如下圖:
Socket通信3要素:
- 通信的目的地址;
- 使用的端口號(hào);
- 使用的傳輸層協(xié)議(如TCP、UDP)
Socket通信模型
Socket被稱(chēng)之為套接字。
在Linux環(huán)境中,Socket編程都是以偽文件的形式運(yùn)行著;既然是文件,我們可以使用文件描述符引用套接字。(Linux一切皆文件)
Linux系統(tǒng)將其封裝成文件的目的是為了統(tǒng)一接口,使得讀寫(xiě)套接字和讀寫(xiě)文件的操作一致。區(qū)別是文件主要應(yīng)用于本地持久化數(shù)據(jù)的讀寫(xiě),而套接字多應(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來(lái)標(biāo)識(shí),那么這兩個(gè)socket組成的socket pair就唯一標(biāo)識(shí)一個(gè)連接。因此可以用Socket來(lái)描述網(wǎng)絡(luò)連接的一對(duì)一關(guān)系。
套接字通信原理如下圖所示:
在網(wǎng)絡(luò)通信中,套接字一定是成對(duì)出現(xiàn)的。一端的發(fā)送緩沖區(qū)對(duì)應(yīng)對(duì)端的接收緩沖區(qū)。我們使用同一個(gè)文件描述符來(lái)發(fā)送緩沖區(qū)和接收緩沖區(qū)。
Socket 通信創(chuàng)建流程圖
二、Socket編程基礎(chǔ)
1. 網(wǎng)絡(luò)字節(jié)序
在計(jì)算機(jī)世界里,有兩種字節(jié)序:
大端字節(jié)序 --- 低地址高字節(jié),高地址低字節(jié)
小端字節(jié)序 --- 低地址低字節(jié),高地址高字節(jié)
內(nèi)存中的多字節(jié)數(shù)據(jù)相對(duì)于內(nèi)存地址有大端和小端之分,磁盤(pán)文件中的多字節(jié)數(shù)據(jù)相對(duì)于文件中的偏移地址也有大端小端之分,網(wǎng)絡(luò)數(shù)據(jù)流同樣有大端小端之分。
網(wǎng)絡(luò)數(shù)據(jù)流的地址有這樣規(guī)定:先發(fā)出的數(shù)據(jù)是低地址,后發(fā)出的數(shù)據(jù)是高地址。
TCP/IP協(xié)議規(guī)定,網(wǎng)絡(luò)數(shù)據(jù)流應(yīng)采用大端字節(jié)序,即低地址高字節(jié)。
所以,我們?cè)诖a中必須要將ip地址和端口號(hào)做相應(yīng)的轉(zhuǎn)換,轉(zhuǎn)換為網(wǎng)絡(luò)字節(jié)序才可以進(jìn)行通訊。
大多數(shù)使用 htonl 和 htons 。
為什么需要轉(zhuǎn)換呢?
假設(shè)本地主機(jī)使用的是小端字節(jié)序,而對(duì)方主機(jī)使用的是大端字節(jié)序;你發(fā)送數(shù)據(jù)過(guò)去的地址順序是:0x06b3,而對(duì)方接受到的卻是:0xb306;這樣數(shù)據(jù)就亂了,所以需要進(jìn)行轉(zhuǎn)換!
需要通過(guò)以下接口進(jìn)行轉(zhuǎn)換:
#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)整數(shù),s表示16位短整數(shù)。
l 結(jié)尾的函數(shù)用于ip地址轉(zhuǎn)換,s 結(jié)尾的函數(shù)用于端口號(hào)的轉(zhuǎn)換。
如果主機(jī)是小端字節(jié)序,這些函數(shù)將參數(shù)做相應(yīng)的大小端轉(zhuǎn)換然后返回,如果主機(jī)是大端字節(jié)序,這些函數(shù)不做轉(zhuǎn)換,將參數(shù)原封不動(dòng)地返回。
2. sockaddr數(shù)據(jù)結(jié)構(gòu)
我們?cè)谑褂胹ocket中,需要使用結(jié)構(gòu)體sockaddr_in將IP地址和端口號(hào)等保存,然后用于綁定socket;
但進(jìn)行綁定時(shí),我們卻要將結(jié)構(gòu)體sockaddr_in強(qiáng)制類(lèi)型轉(zhuǎn)換為結(jié)構(gòu)體sockaddr,這是為什么呢?
由于歷史原因,一開(kāi)始是沒(méi)有結(jié)構(gòu)體sockaddr_in,只有結(jié)構(gòu)體sockaddr。
后來(lái)為了適配ipv4的到來(lái),將結(jié)構(gòu)體sockaddr細(xì)化為結(jié)構(gòu)體sockaddr_in,如上圖。
兩個(gè)結(jié)構(gòu)體如下:
struct sockaddr { sa_family_t sa_family; /* address family, AF_xxx */ 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í)還沒(méi)有void *類(lèi)型,因此這些像bind 、accept函數(shù)的參數(shù)都用struct sockaddr *類(lèi)型表示,在傳遞參數(shù)之前要強(qiáng)制類(lèi)型轉(zhuǎn)換一下,
例如:
struct sockaddr_in servaddr;
bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr)); /* initialize servaddr */
3. IP地址轉(zhuǎn)換函數(shù)
上面網(wǎng)絡(luò)字節(jié)序中我們使用了htonl 和 ntohl 兩個(gè)函數(shù)進(jìn)行ip地址的轉(zhuǎn)換,但只能將uint32_t類(lèi)型的地址進(jìn)行轉(zhuǎn)換,例如:INADDR_ANY ==> 0.0.0.0
但是實(shí)際項(xiàng)目中我們?cè)O(shè)置ip地址大多數(shù)都是字符串,所以得使用特定的函數(shù)去進(jìn)行轉(zhuǎn)換。
#include <arpa/inet.h>
int inet_pton (int af, const char *src, void *dst); // “本地ip轉(zhuǎn)換為網(wǎng)絡(luò)ip”
const char *inet_ntop (int af, const void *src, char *dst, socklen_t size); // “網(wǎng)絡(luò)ip轉(zhuǎn)換為本地ip”
af 取值可選為 AF_INET 和 AF_INET6 ,即和 ipv4 和ipv6對(duì)應(yīng)支持IPv4和IPv6;
src 是轉(zhuǎn)換前ip,dst 是轉(zhuǎn)換后ip;
其中inet_pton和inet_ntop不僅可以轉(zhuǎn)換IPv4的in_addr,還可以轉(zhuǎn)換IPv6的in6_addr。
例:
#include <stdio.h> #include <string.h> #include <arpa/inet.h> int main(int argc, char *argv[]) { char ip[] = "6.7.8.9"; char server_ip[64]; struct sockaddr_in server_addr; inet_pton(AF_INET, ip, &server_addr.sin_addr.s_addr); printf("s_addr : %x\n", server_addr.sin_addr.s_addr); printf("s_addr from net to host : %x \n", ntohl(server_addr.sin_addr.s_addr)); inet_ntop(AF_INET, &server_addr.sin_addr.s_addr, server_ip, sizeof(server_ip)); printf("server_ip : %s \n", server_ip); printf("INADDR_ANY: %d \n", INADDR_ANY); server_addr.sin_addr.s_addr = htonl(INADDR_ANY); inet_ntop(AF_INET, &server_addr.sin_addr.s_addr, server_ip, sizeof(server_ip)); printf("INADDR_ANY ip : %s\n", server_ip); return 0; }
運(yùn)行結(jié)果:
s_addr : 9080706
s_addr from net to host : 6070809
server_ip : 6.7.8.9
INADDR_ANY: 0
INADDR_ANY ip : 0.0.0.0
ip地址:6.7.8.9
因?yàn)榫W(wǎng)絡(luò)上使用的是大端字節(jié)序,所以通過(guò)inet_pton函數(shù)轉(zhuǎn)換后的ip地址輸出為:9080706
當(dāng)通過(guò)ntohl函數(shù)轉(zhuǎn)換回主機(jī)ip地址后輸出為:6070809
因?yàn)槲业谋镜刂鳈C(jī)使用的是小段字節(jié)序,所以轉(zhuǎn)換后的循序和ip地址順序一致,大端字節(jié)序則反過(guò)來(lái)了;
如果需要將網(wǎng)絡(luò)的ip地址轉(zhuǎn)換為字符串,則需要使用inet_ntop函數(shù);
如果需要將字符串ip地址轉(zhuǎn)換為網(wǎng)絡(luò)ip地址,則需要使用inet_pton函數(shù)。
在服務(wù)器中,如果有多個(gè)網(wǎng)絡(luò),一般我們都會(huì)綁定所有網(wǎng)卡,會(huì)進(jìn)行如下設(shè)置:
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 監(jiān)聽(tīng)本地所有IP地址
INADDR_ANY是一個(gè)宏,即為0的宏,他轉(zhuǎn)換后賦值給結(jié)構(gòu)體實(shí)際上是:0.0.0.0這個(gè)ip地址。
三、Socket編程函數(shù)
1. socket函數(shù)
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket (int domain, int type, int protocol);
domain:
AF_INET 這是大多數(shù)用來(lái)產(chǎn)生socket的協(xié)議,使用TCP或UDP來(lái)傳輸,用IPv4的地址。
AF_INET6 與上面類(lèi)似,不過(guò)是來(lái)用IPv6的地址。
AF_UNIX 本地協(xié)議,使用在Unix和Linux系統(tǒng)上,一般都是當(dāng)客戶(hù)端和服務(wù)器在同一臺(tái)及其上的時(shí)候使用。
type:
SOCK_STREAM 這個(gè)協(xié)議是按照順序的、可靠的、數(shù)據(jù)完整的基于字節(jié)流的連接。這是一個(gè)使用最多的socket類(lèi)型,這個(gè)socket是使用TCP來(lái)進(jìn)行傳輸。
SOCK_DGRAM 這個(gè)協(xié)議是無(wú)連接的、固定長(zhǎng)度的傳輸調(diào)用。該協(xié)議是不可靠的,使用UDP來(lái)進(jìn)行它的連接。
SOCK_SEQPACKET 該協(xié)議是雙線路的、可靠的連接,發(fā)送固定長(zhǎng)度的數(shù)據(jù)包進(jìn)行傳輸。必須把這個(gè)包完整的接受才能進(jìn)行讀取。
SOCK_RAW socket類(lèi)型提供單一的網(wǎng)絡(luò)訪問(wèn),這個(gè)socket類(lèi)型使用ICMP公共協(xié)議。(ping、traceroute使用該協(xié)議)。
SOCK_RDM 這個(gè)類(lèi)型是很少使用的,在大部分的操作系統(tǒng)上沒(méi)有實(shí)現(xiàn),它是提供給數(shù)據(jù)鏈路層使用,不保證數(shù)據(jù)包的順序。
protocol:
傳 0 表示使用默認(rèn)協(xié)議。
返回值:
成功:返回指向新創(chuàng)建的socket的文件描述符,失敗:返回 -1,設(shè)置errno
可以使用以下方式進(jìn)行打印輸出失敗報(bào)錯(cuò)信息:
fprintf(stderr, " errno:%s\n", strerror(errno));
socket()打開(kāi)一個(gè)網(wǎng)絡(luò)通訊端口,如果成功的話(huà),就像open()一樣返回一個(gè)文件描述符,應(yīng)用程序可以像讀寫(xiě)文件一樣用read/write在網(wǎng)絡(luò)上收發(fā)數(shù)據(jù),如果socket()調(diào)用出錯(cuò)則返回-1。
對(duì)于IPv4,domain 參數(shù)指定為AF_INET。
對(duì)于TCP協(xié)議,type 參數(shù)指定為SOCK_STREAM,表示面向流的傳輸協(xié)議。如果是UDP協(xié)議,則type參數(shù)指定為SOCK_DGRAM,表示面向數(shù)據(jù)報(bào)的傳輸協(xié)議。
protocol 參數(shù)的介紹 - 略,指定為0即可。
例:
int sock; sock = socket(AF_INET, SOCK_STREAM, 0);
2. bind 函數(shù)
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind (int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:
socket文件描述符
addr:
構(gòu)造出IP地址加端口號(hào)的結(jié)構(gòu)體
addrlen:
sizeof(addr)長(zhǎng)度
返回值:
成功 返回 0,失敗 返回 -1, 設(shè)置 errno
可以使用以下方式進(jìn)行打印輸出失敗報(bào)錯(cuò)信息:
fprintf(stderr, " errno:%s\n", strerror(errno));
服務(wù)器程序所監(jiān)聽(tīng)的網(wǎng)絡(luò)地址和端口號(hào)通常是固定不變的,客戶(hù)端程序得知服務(wù)器程序的地址和端口號(hào)后就可以向服務(wù)器發(fā)起連接,因此服務(wù)器需要調(diào)用bind綁定一個(gè)固定的網(wǎng)絡(luò)地址和端口號(hào)。
bind()的作用是將參數(shù)sockfd和addr綁定在一起,使sockfd這個(gè)用于網(wǎng)絡(luò)通訊的文件描述符監(jiān)聽(tīng)addr所描述的地址和端口號(hào)。
例:
struct sockaddr_in servaddr; // 定義結(jié)構(gòu)體 bzero(&servaddr, sizeof(servaddr)); // 將整個(gè)結(jié)構(gòu)體清零 // 設(shè)置地址類(lèi)型為AF_INET(IPv4) servaddr.sin_family = AF_INET; /* 網(wǎng)絡(luò)地址為INADDR_ANY,這個(gè)宏表示本地的任意IP地址,因?yàn)榉?wù)器可能有多個(gè)網(wǎng)卡, 每個(gè)網(wǎng)卡也可能綁定多個(gè)IP地址,這樣設(shè)置可以在所有的IP地址上監(jiān)聽(tīng), 直到與某個(gè)客戶(hù)端建立了連接時(shí)才確定下來(lái)到底用哪個(gè)IP地址. */ servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 設(shè)置端口號(hào)為5000 servaddr.sin_port = htons(5000); // 綁定 bind(sock, (struct sockaddr *)&servaddr, sizeof(servaddr));
3. listen 函數(shù)
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen (int sockfd, int backlog);
sockfd:
socket 文件描述符
backlog:
在Linux 系統(tǒng)中,它是指排隊(duì)等待建立3次握手隊(duì)列長(zhǎng)度。(客戶(hù)端同時(shí)進(jìn)行連接服務(wù)器的個(gè)數(shù))
返回值:
成功 返回 0,失敗 返回 -1, 設(shè)置 errno
可以使用以下方式進(jìn)行打印輸出失敗報(bào)錯(cuò)信息:
fprintf(stderr, " errno:%s\n", strerror(errno));
查看一下系統(tǒng)默認(rèn)backlog
cat /proc/sys/net/ipv4/tcp_max_syn_backlog
改變 系統(tǒng)限制的backlog 大小
1. 打開(kāi)文件
vim /etc/sysctl.conf
2. 在文件最后添加
net.core.somaxconn = 1024
net.ipv4.tcp_max_syn_backlog = 1024
3. 保存,然后執(zhí)行
sysctl -p
如下圖:(修改系統(tǒng)默認(rèn)backlog為1024)
為什么要修改呢?如果不修改,即使我們?cè)诖a里設(shè)置10240(listen(sock, 10240);),它也還是按照系統(tǒng)默認(rèn)的值來(lái)設(shè)置的!
典型的服務(wù)器程序可以同時(shí)服務(wù)于多個(gè)客戶(hù)端,當(dāng)有客戶(hù)端發(fā)起連接時(shí),服務(wù)器調(diào)用的accept()返回并接受這個(gè)連接,如果有大量的客戶(hù)端發(fā)起連接而服務(wù)器來(lái)不及處理,尚未accept的客戶(hù)端就處于連接等待狀態(tài),listen()聲明sockfd處于監(jiān)聽(tīng)狀態(tài),并且最多允許有backlog個(gè)客戶(hù)端處于連接待狀態(tài),如果接收到更多的連接請(qǐng)求就忽略。
例:
// 監(jiān)聽(tīng),同時(shí)監(jiān)聽(tīng)128個(gè)請(qǐng)求 listen(sock, 128);
4. accept 函數(shù)
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept (int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockdf:
socket文件描述符
addr:
傳出參數(shù),返回連接客戶(hù)端地址信息,含IP地址和端口號(hào)
addrlen:
傳入傳出參數(shù)(值-結(jié)果),傳入sizeof(addr)大小,函數(shù)返回時(shí)返回真正接收到地址結(jié)構(gòu)體的大小
返回值:
成功返回一個(gè)新的socket文件描述符,用于和客戶(hù)端通信,失敗返回 -1,設(shè)置errno
可以使用以下方式進(jìn)行打印輸出失敗報(bào)錯(cuò)信息:
fprintf(stderr, " errno:%s\n", strerror(errno));
三次握手完成后,服務(wù)器調(diào)用accept()接受連接,如果服務(wù)器調(diào)用accept()時(shí)還沒(méi)有客戶(hù)端的連接請(qǐng)求,就阻塞等待直到有客戶(hù)端連接上來(lái)。
addr 是一個(gè)傳出參數(shù),accept()返回時(shí)傳出客戶(hù)端的地址和端口號(hào);如果給addr參數(shù)傳NULL,表示不關(guān)心客戶(hù)端的地址。
addrlen 參數(shù)是一個(gè)傳入傳出參數(shù)(value-result argument),傳入的是調(diào)用者提供的緩沖區(qū)addr的長(zhǎng)度以避免緩沖區(qū)溢出問(wèn)題,傳出的是客戶(hù)端地址結(jié)構(gòu)體的實(shí)際長(zhǎng)度(有可能沒(méi)有占滿(mǎn)調(diào)用者提供的緩沖區(qū))。
例:
struct sockaddr_in client; int client_sock; socklen_t client_addr_len; client_addr_len = sizeof(client); // 接受 client_sock = accept(sock, (struct sockaddr *)&client, &client_addr_len);
5. connect 函數(shù)
客戶(hù)端使用!
#include <sys/types.h> /* See NOTES */
#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ù),傳入sizeof(addr)大小
返回值:
成功 返回 0,失敗 返回 -1, 設(shè)置 errno
可以使用以下方式進(jìn)行打印輸出失敗報(bào)錯(cuò)信息:
fprintf(stderr, " errno:%s\n", strerror(errno));
客戶(hù)端需要調(diào)用connect()連接服務(wù)器,connect和bind的參數(shù)形式一致,區(qū)別在于bind的參數(shù)是自己的地址,而connect的參數(shù)是對(duì)方的地址。
例:
int sockfd = 0; struct sockaddr_in serveraddr; sockfd = socket(AF_INET, SOCK_STREAM, 0); serveraddr.sin_family = AF_INET; inet_pton(AF_INET, "127.0.0.1", &serveraddr.sin_addr); serveraddr.sin_port = htons(5000); // 連接服務(wù)器 connect(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
6. read 函數(shù)
#include <unistd.h>
ssize_t read (int fd, void *buf, size_t count);
fd:
socket文件描述符;
buf:
存儲(chǔ)讀取到的數(shù)據(jù),一般傳char *類(lèi)型或字符數(shù)組;
count:
指定最多讀取的大小。
返回值:
讀取成功返回讀取到的字節(jié)數(shù),讀取失敗返回 -1,設(shè)置errno
可以使用以下方式進(jìn)行打印輸出失敗報(bào)錯(cuò)信息:
fprintf(stderr, " errno:%s\n", strerror(errno));
從socket文件符中,讀取count指定的大小以?xún)?nèi)的數(shù)據(jù)存儲(chǔ)到buf中。
例:
int client_sock; char buf[256]; // 對(duì)client_sock的賦值這里省略... len = read(client_sock, buf, sizeof(buf)-1);
7. write 函數(shù)
#include <unistd.h>
ssize_t write (int fd, const void *buf, size_t count);
fd:
socket文件描述符;
buf:
需要發(fā)送(寫(xiě)入)的數(shù)據(jù);
count:
指定最多發(fā)送(寫(xiě)入)的大小。
返回值:
發(fā)送(寫(xiě)入)成功返回寫(xiě)入的字節(jié)數(shù),發(fā)送(寫(xiě)入)失敗返回 -1,設(shè)置errno
可以使用以下方式進(jìn)行打印輸出失敗報(bào)錯(cuò)信息:
fprintf(stderr, " errno:%s\n", strerror(errno));
例:
int client_sock; char buf[256]; // 對(duì)client_sock的賦值這里省略... len = write(client_sock, buf, sizeof(buf)-1);
8. close 函數(shù)
#include <unistd.h>
int close (int fd);
fd:
socket文件描述符;
返回值:
成功返回 0,失敗返回 -1,并適當(dāng)設(shè)置errno。
可以使用以下方式進(jìn)行打印輸出失敗報(bào)錯(cuò)信息:
fprintf(stderr, " errno:%s\n", strerror(errno));
close()關(guān)閉一個(gè)文件描述符。
例:
int client_sock; // client_sock= socket(AF_INET, SOCK_STREAM, 0); close(client_sock);
四、回聲服務(wù)器案例
描述:
客戶(hù)端連接服務(wù)器,給服務(wù)器發(fā)送“hello world!”,服務(wù)器接收到后,將信息打印輸出后,原封不動(dòng)的給客戶(hù)端發(fā)送回去,客戶(hù)端接收到到后,也就數(shù)據(jù)打印輸出,程序結(jié)束。
1. 服務(wù)器
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <string.h> // strerror #include <ctype.h> #include <arpa/inet.h> #include <netinet/in.h> #include <stdlib.h> #include <errno.h> #define SERVER_PORT 5000 int main(void) { int ret = 0; int sock; // 通信套接字 struct sockaddr_in server_addr; // 1.創(chuàng)建通信套接字 sock = socket(AF_INET, SOCK_STREAM, 0); if (-1 == sock) { fprintf(stderr, "create socket error, reason: %s\n", strerror(errno)); exit(-1); } // 2.清空標(biāo)簽,寫(xiě)上地址和端口號(hào) bzero(&server_addr, sizeof(server_addr)); server_addr.sin_family = AF_INET; // 選擇協(xié)議組ipv4 server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 監(jiān)聽(tīng)本地所有IP地址 server_addr.sin_port = htons(SERVER_PORT); // 綁定端口號(hào) // 3.綁定 ret = bind(sock, (struct sockaddr *)&server_addr, sizeof(server_addr)); if (-1 == ret) { fprintf(stderr, "socket bind error, reason: %s\n", strerror(errno)); close(sock); exit(-2); } // 4.監(jiān)聽(tīng),同時(shí)監(jiān)聽(tīng)128個(gè)請(qǐng)求 ret = listen(sock, 128); if (-1 == ret) { fprintf(stderr, "listen error, reason: %s\n", strerror(errno)); close(sock); exit(-2); } printf("等待客戶(hù)端的鏈接\n"); int done = 1; while (done) { struct sockaddr_in client; int client_sock; char client_ip[64]; int len = 0; char buf[256]; socklen_t client_addr_len; client_addr_len = sizeof(client); // 5.接受 client_sock = accept(sock, (struct sockaddr *)&client, &client_addr_len); if (-1 == client_sock) { perror("accept error"); close(sock); exit(-3); } // 打印客戶(hù)端IP地址和端口號(hào) printf("client ip: %s\t port: %d\n", inet_ntop(AF_INET, &client.sin_addr.s_addr, client_ip, sizeof(client_ip)), ntohs(client.sin_port)); // 6.讀取客戶(hù)端發(fā)送的數(shù)據(jù) len = read(client_sock, buf, sizeof(buf)-1); if (-1 == len) { perror("read error"); close(sock); close(client_sock); exit(-4); } buf[len] = '\0'; printf("recive[%d]: %s\n", len, buf); // 7.給客戶(hù)端發(fā)送數(shù)據(jù) len = write(client_sock, buf, len); if (-1 == len) { perror("write error"); close(sock); close(client_sock); exit(-5); } printf("write finished. len: %d\n", len); // 8.關(guān)閉客戶(hù)端套接字 close(client_sock); } // 9.關(guān)閉服務(wù)器套接字 close(sock); return 0; }
2. 客戶(hù)端
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <errno.h> #define SERVER_PORT 5000 #define SERVER_IP "127.0.0.1" int main(int argc, char *argv[]) { int ret = 0; int sockfd = 0; // 通信套接字 char *message = NULL; struct sockaddr_in serveraddr; int n = 0; char buff[64]; if (2 != argc) { fprintf(stderr, "Usage: ./echo_client message \n"); exit(1); } // 獲取第二個(gè)參數(shù)的字符串 message = argv[1]; printf("message: %s\n", message); // 1.創(chuàng)建通信套接字 sockfd = socket(AF_INET, SOCK_STREAM, 0); if (-1 == sockfd) { perror("create sockfd error"); exit(-1); } // 2.清空標(biāo)簽,寫(xiě)上地址和端口號(hào) bzero(&serveraddr, sizeof(serveraddr)); serveraddr.sin_family = AF_INET; // IPv4 inet_pton(AF_INET, SERVER_IP, &serveraddr.sin_addr); // 服務(wù)器地址 serveraddr.sin_port = htons(SERVER_PORT); // 服務(wù)器端口號(hào) // 3.連接服務(wù)器 ret = connect(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)); if (-1 == ret) { perror("connect error"); close(sockfd); exit(-2); } // 4.給服務(wù)器發(fā)送數(shù)據(jù) ret = write(sockfd, message, strlen(message)); if (-1 == ret) { perror("write error"); close(sockfd); exit(-3); } // 5.接受服務(wù)器發(fā)送過(guò)來(lái)的數(shù)據(jù) n = read(sockfd, buff, sizeof(buff)-1); if (-1 == n) { perror("read error"); close(sockfd); exit(-4); } if (n > 0) { buff[n] = '\0'; printf("receive: %s\n", buff); } else { perror("error!!!\n"); } printf("client finished.\n"); // 6.關(guān)閉套接字 close(sockfd); return 0; }
3. 運(yùn)行測(cè)試
1. 服務(wù)器
ygt@YGT:~/echo_server$ gcc echo_server.c -o echo_server ygt@YGT:~/echo_server$ ./echo_server 等待客戶(hù)端的鏈接 client ip: 127.0.0.1 port: 41168 recive[12]: hello world! write finished. len: 12
這里打印客戶(hù)端的IP地址是127.0.0.1,是因?yàn)槲沂窃谕慌_(tái)Linux系統(tǒng)中進(jìn)行測(cè)試的,所以打印的是這個(gè)本地地址。
2. 客戶(hù)端
root@YGT:/home/ygt/echo_server# gcc echo_client.c -o echo_client root@YGT:/home/ygt/echo_server# ./echo_client "hello world!" message: hello world! receive: hello world! client finished.
再來(lái)測(cè)試一下,在Linux中運(yùn)行服務(wù)器程序,然后再window環(huán)境使用cmd控制臺(tái)敲命令telnet去連接服務(wù)器。
才cmd中,telnet 后面接 服務(wù)器的ip地址 和 端口號(hào)
當(dāng)按下回車(chē)鍵后,就連接上服務(wù)器了,服務(wù)器也接受到了客戶(hù)端的IP地址和端口號(hào),并將其打印出來(lái);然后客戶(hù)端將字符 ‘h’ 發(fā)送給了服務(wù)器,服務(wù)器接收到后將其打印出來(lái),然后給客戶(hù)端也發(fā)送字符 'h',但是我們?cè)赾md上是沒(méi)有接收功能的,所以就沒(méi)有接收到服務(wù)器發(fā)送過(guò)來(lái)的消息;最后服務(wù)器發(fā)送完成后就close斷開(kāi)了和客戶(hù)端的連接,cmd這邊就提示“遺失對(duì)主機(jī)的連接”。
五、總結(jié)
Linux環(huán)境中的C/C++ socket 與Window環(huán)境中的C/C++ socket類(lèi)似。
創(chuàng)建服務(wù)器時(shí)需要按照指定流程來(lái)創(chuàng)建,根據(jù)上面圖Socket 通信創(chuàng)建流程圖來(lái)創(chuàng)建即可。
注意調(diào)用系統(tǒng)函數(shù)失敗時(shí),可以打印失敗原因幫助我們定位問(wèn)題。
到此這篇關(guān)于C/C++ Linux Socket網(wǎng)絡(luò)編程的文章就介紹到這了,更多相關(guān)C/C++ Linux Socket網(wǎng)絡(luò)編程內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- 詳解從Linux源碼看Socket(TCP)的bind
- 詳解Linux使用ss命令結(jié)合zabbix對(duì)socket做監(jiān)控
- 從Linux源碼看Socket(TCP)Client端的Connect的示例詳解
- 局域網(wǎng)內(nèi)python socket實(shí)現(xiàn)windows與linux間的消息傳送
- Linux進(jìn)程間通信方式之socket使用實(shí)例
- linux下socket編程常用頭文件(推薦)
- linux socket通訊獲取本地的源端口號(hào)的實(shí)現(xiàn)方法
- Linux UDP socket 設(shè)置為的非阻塞模式與阻塞模式區(qū)別
- Linux?socket函數(shù)詳解
相關(guān)文章
C語(yǔ)言中輸入函數(shù)(scanf()、fgets()和gets())的區(qū)別詳解
這篇文章主要給大家介紹了關(guān)于C語(yǔ)言中三種輸入函數(shù)(scanf()、fgets()和gets())區(qū)別的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2017-11-11c++ STL容器總結(jié)之:vertor與list的應(yīng)用
本篇文章對(duì)c++中STL容器中的vertor與list的應(yīng)用進(jìn)行了詳細(xì)的分析解釋。需要的朋友參考下2013-05-05OpenCV圖像算法實(shí)現(xiàn)圖像切分圖像合并示例
這篇文章主要為大家介紹了python圖像算法OpenCV實(shí)現(xiàn)圖像切分圖像合并操作示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06