實(shí)現(xiàn)一個(gè)基于socket的echo服務(wù)端和客戶端
成都創(chuàng)新互聯(lián)公司-專業(yè)網(wǎng)站定制、快速模板網(wǎng)站建設(shè)、高性價(jià)比古田網(wǎng)站開發(fā)、企業(yè)建站全套包干低至880元,成熟完善的模板庫,直接使用。一站式古田網(wǎng)站制作公司更省心,省錢,快速模板網(wǎng)站建設(shè)找我們,業(yè)務(wù)覆蓋古田地區(qū)。費(fèi)用合理售后完善,10多年實(shí)體公司更值得信賴。
在linux中,一切都是文件,所有文件都有一個(gè)int類型的編號(hào),稱為文件描述符。服務(wù)端和客戶端通信本質(zhì)是在各自機(jī)器上創(chuàng)建一個(gè)文件,稱為socket(套接字),然后對(duì)該socket文件進(jìn)行讀寫。
在 Linux 下使用 <sys/socket.h>
頭文件中 socket() 函數(shù)來創(chuàng)建套接字
int socket(int af, int type, int protocol);
AF_INET
, IPv6填AF_INET6
SOCK_STREAM
表示流格式、面向連接,多用于TCP。SOCK_DGRAM
表示數(shù)據(jù)報(bào)格式、無連接,多用于UDPIPPTOTO_UDP
表示UDP。可直接填0
,會(huì)自動(dòng)根據(jù)前面的兩個(gè)參數(shù)自動(dòng)推導(dǎo)協(xié)議類型#include <sys/socket.h>
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
socket()函數(shù)創(chuàng)建出套接字后,套接字中并沒有任何地址信息。需要用bind()函數(shù)將套接字和監(jiān)聽的IP和端口綁定起來,這樣當(dāng)有數(shù)據(jù)到該IP和端口時(shí),系統(tǒng)才知道需要交給綁定的套接字處理。
bind函數(shù)也在<sys/socket.h>
頭文件中,原型為:
int bind(int sock, struct sockaddr *addr, socklen_t addrlen);
我們先看看socket和bind的綁定代碼,下面代碼中,我們將創(chuàng)建的socket與ip='127.0.0.1',port=8888進(jìn)行綁定:
#include <sys/socket.h>
#include <netinet/in.h>
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr)); //用0填充
server_addr.sin_family = AF_INET; //使用IPv4地址
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具體的IP地址;填入INADDR_ANY表示"0.0.0.0"
server_addr.sin_port = htons(8888); //端口
//將套接字和IP、端口綁定
bind(server_addr, (struct sockaddr*)&server_addr, sizeof(server_addr));
可以看到,我們使用sockaddr_in結(jié)構(gòu)體設(shè)置要綁定的地址信息,然后再強(qiáng)制轉(zhuǎn)換為sockaddr類型。這是為了讓bind函數(shù)能適應(yīng)多種協(xié)議。
struct sockaddr_in{
sa_family_t sin_family; //地址族(Address Family),也就是地址類型
uint16_t sin_port; //16位的端口號(hào)
struct in_addr sin_addr; //32位IP地址
char sin_zero[8]; //不使用,一般用0填充
};
struct sockaddr_in6 {
sa_family_t sin6_family; //(2)地址類型,取值為AF_INET6
in_port_t sin6_port; //(2)16位端口號(hào)
uint32_t sin6_flowinfo; //(4)IPv6流信息
struct in6_addr sin6_addr; //(4)具體的IPv6地址
uint32_t sin6_scope_id; //(4)接口范圍ID
};
struct sockaddr{
sa_family_t sin_family; //地址族(Address Family),也就是地址類型
char sa_data[14]; //IP地址和端口號(hào)
};
其中,sockaddr_in是保存IPv4的結(jié)構(gòu)體;sockadd_in6是保存IPv6的結(jié)構(gòu)體;sockaddr是通用的結(jié)構(gòu)體,通過將特定協(xié)議的結(jié)構(gòu)體轉(zhuǎn)換成sockaddr,以達(dá)到bind可綁定多種協(xié)議的目的。
注意在設(shè)置server_addr的端口號(hào)時(shí),需要使用htons函數(shù)將傳進(jìn)來的端口號(hào)轉(zhuǎn)換成大端字節(jié)序
計(jì)算機(jī)硬件有兩種儲(chǔ)存數(shù)值的方式:大端字節(jié)序和小端字節(jié)序
大端字節(jié)序指數(shù)值的高位字節(jié)存在前面(低內(nèi)存地址),低位字節(jié)存在后面(高內(nèi)存地址)。
小端字節(jié)序則反過來,低位字節(jié)存在前面,高位字節(jié)存在后面。
計(jì)算機(jī)電路先處理低位字節(jié),效率比較高,因?yàn)橛?jì)算都是從低位開始的。而計(jì)算機(jī)讀內(nèi)存數(shù)據(jù)都是從低地址往高地址讀。所以,計(jì)算機(jī)的內(nèi)部是小端字節(jié)序。但是,人類還是習(xí)慣讀寫大端字節(jié)序。除了計(jì)算機(jī)的內(nèi)部處理,其他的場(chǎng)合比如網(wǎng)絡(luò)傳輸和文件儲(chǔ)存,幾乎都是用的大端字節(jié)序。
linux在頭文件<arpa/inet.h>
提供了htonl/htons用于將數(shù)值轉(zhuǎn)化為網(wǎng)絡(luò)傳輸使用的大端字節(jié)序儲(chǔ)存;對(duì)應(yīng)的有ntohl/ntohs用于將數(shù)值從網(wǎng)絡(luò)傳輸使用的大端字節(jié)序轉(zhuǎn)化為計(jì)算機(jī)使用的字節(jié)序
int listen(int sock, int backlog); //Linux
半連接隊(duì)列&全連接隊(duì)列:我們都知道tcp的三次握手,在第一次握手時(shí),服務(wù)端收到客戶端的SYN后,會(huì)把這個(gè)連接放入半連接隊(duì)列中。然后發(fā)送ACK+SYN。在收到客戶端的ACK回包后,握手完成,會(huì)把連接從半連接隊(duì)列移到全連接隊(duì)列中,等待處理。
調(diào)用listen后,此時(shí)客戶端就可以和服務(wù)端三次握手建立連接了,但建立的連接會(huì)被放到全連接隊(duì)列中。accept就是從這個(gè)隊(duì)列中獲取客戶端請(qǐng)求。每調(diào)用一次accept,會(huì)從隊(duì)列中獲取一個(gè)客戶端請(qǐng)求。
int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);
accpet返回一個(gè)新的套接字,之后服務(wù)端用這個(gè)套接字與連接對(duì)應(yīng)的客戶端進(jìn)行通信。
在沒請(qǐng)求進(jìn)來時(shí)調(diào)用accept會(huì)阻塞程序,直到新的請(qǐng)求進(jìn)來。
至此,我們就講完了服務(wù)端的監(jiān)聽流程,接下來我們可以先調(diào)用read等待讀入客戶端發(fā)過來的數(shù)據(jù),然后再調(diào)用write向客戶端發(fā)送數(shù)據(jù)。再用close把a(bǔ)ccept_fd關(guān)閉,斷開連接。完整代碼如下
// server.cpp
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <cstdio>
#include <errno.h>
int main() {
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
server_addr.sin_port = htons(8888);
if (bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
printf("bind err: %s\n", strerror(errno));
close(listen_fd);
return -1;
}
if (listen(listen_fd, 2048) < 0) {
printf("listen err: %s\n", strerror(errno));
close(listen_fd);
return -1;
}
struct sockaddr_in client_addr;
bzero(&client_addr, sizeof(struct sockaddr_in));
socklen_t client_addr_len = sizeof(client_addr);
int accept_fd = 0;
while((accept_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &client_addr_len)) > 0) {
printf("get accept_fd: %d from: %s:%d\n", accept_fd, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
char read_msg[100];
int read_num = read(accept_fd, read_msg, 100);
printf("get msg from client: %s\n", read_msg);
int write_num = write(accept_fd, read_msg, read_num);
close(accept_fd);
}
}
[C++小知識(shí)] 在使用printf打印調(diào)試信息時(shí),由于系統(tǒng)緩沖區(qū)問題,如果不加"\n",有時(shí)會(huì)打印不出來字符串。
C提供的很多函數(shù)調(diào)用產(chǎn)生錯(cuò)誤時(shí),會(huì)將錯(cuò)誤碼賦值到一個(gè)全局int變量errno上,可以通過strerror(errno)輸入具體的報(bào)錯(cuò)信息
客戶端就比較簡單了,創(chuàng)建一個(gè)sockaddr_in
變量,填充服務(wù)端的ip和端口,通過connect調(diào)用就可以獲取到一個(gè)與服務(wù)端通信的套接字。
int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen);
各個(gè)參數(shù)的說明和bind()相同,不再重復(fù)。
創(chuàng)建連接后,我們先調(diào)write向服務(wù)端發(fā)送數(shù)據(jù),再調(diào)用read等待讀入服務(wù)端發(fā)過來的數(shù)據(jù),然后調(diào)用close斷開連接。完整代碼如下:
// client.cpp
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <cstdio>
#include <iostream>
int main() {
int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
server_addr.sin_port = htons(8888);
if (connect(sock_fd, (sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
printf("connect err: %s\n", strerror(errno));
return -1;
};
printf("success connect to server\n");
char input_msg[100];
// 等待輸入數(shù)據(jù)
std::cin >> input_msg;
printf("input_msg: %s\n", input_msg);
int write_num = write(sock_fd, input_msg, 100);
char read_msg[100];
int read_num = read(sock_fd, read_msg, 100);
printf("get from server: %s\n", read_msg);
close(sock_fd);
}
分別編譯后,我們就得到了一個(gè)echo服務(wù)的服務(wù)端和客戶端
~# ./server
get accept_fd: 4 from: 127.0.0.1:
get msg from client: abc
~# ./client
abc
input_msg: abc
get from server: abc
完整源碼已上傳到CProxy-tutorial,歡迎fork and star!
先啟動(dòng)server,然后啟動(dòng)一個(gè)client,不輸入數(shù)據(jù),這個(gè)時(shí)候在另外一個(gè)終端上再啟動(dòng)一個(gè)client,并在第二個(gè)client終端中輸入數(shù)據(jù),會(huì)發(fā)生什么呢?
如果本文對(duì)你有用,點(diǎn)個(gè)贊再走吧!或者關(guān)注我,我會(huì)帶來更多優(yōu)質(zhì)的內(nèi)容。
分享文章:【系列教程】 從一個(gè)基礎(chǔ)的socket服務(wù)說起
本文網(wǎng)址:http://chinadenli.net/article40/dsoipho.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供搜索引擎優(yōu)化、手機(jī)網(wǎng)站建設(shè)、App開發(fā)、網(wǎng)站維護(hù)、品牌網(wǎng)站制作、虛擬主機(jī)
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)