筆者在工作中,常常接觸到網(wǎng)絡(luò)通訊相關(guān)的內(nèi)容,經(jīng)常需要著手解決一些網(wǎng)絡(luò)通訊相關(guān)的疑難雜癥。排查網(wǎng)絡(luò)問(wèn)題的時(shí)候,往往需要借助一些工具,而很多時(shí)候自己想要的功能,網(wǎng)上又未能找到匹配度高的exe工具。無(wú)奈之下,有的時(shí)候就不能不自己碼代碼,寫(xiě)一些【為我所用】的測(cè)試代碼,來(lái)幫助自己完成問(wèn)題的排查。
? 本文主要介紹一個(gè)TCP服務(wù)器端的測(cè)試程序,它的主要功能是:接收TCP客戶端的連接,當(dāng)收到客戶端發(fā)送的消息后,立刻給客戶端回復(fù)收到的消息;這個(gè)功能,通俗來(lái)講,就叫【回顯】。別看它很簡(jiǎn)單,但是在實(shí)際排查網(wǎng)絡(luò)問(wèn)題時(shí),確實(shí)非常地有效。
? 通過(guò)本文的閱讀,你將了解到以下內(nèi)容:
- TCP客戶端/服務(wù)器代碼邏輯的剖析
- TCP服務(wù)器端如何獲取客戶端的IP地址和端口信息
- TCP回顯測(cè)試服務(wù)器的使用和驗(yàn)證
? 鑒于筆者主要集中在Linux環(huán)境編程,以下所有講解都是基于Linux環(huán)境;如在Windows環(huán)境下編程,可能需要更改相應(yīng)的網(wǎng)絡(luò)編程API,修改后的功能讀者自行驗(yàn)證。
TCP客戶端/服務(wù)器代碼邏輯的剖析
? 在Linux環(huán)境下,要實(shí)現(xiàn)網(wǎng)絡(luò)通訊,我們一般采用的都是socket編程;但是,Linux環(huán)境下的socket編程是一個(gè)大類(lèi),并不僅僅只有網(wǎng)絡(luò)編程才是socket編程,有一種叫Unix Domain Socket編程,它也叫socket編程。只不過(guò)它一般不用于遠(yuǎn)程的網(wǎng)絡(luò)通訊,而是用于本地(當(dāng)前主機(jī)環(huán)境內(nèi))進(jìn)程之間的通訊。曾經(jīng)就因?yàn)檫@個(gè)問(wèn)題,筆者在一次面試中,就被見(jiàn)多識(shí)廣的面試官DISS了一番,希望大家也補(bǔ)補(bǔ)這方面的知識(shí)。以下部分講述的主要是基于局域網(wǎng)或廣域網(wǎng)的網(wǎng)絡(luò)socket編程。
? 在網(wǎng)絡(luò)socket編程中,會(huì)有2種不同的【身份】:客戶端和服務(wù)器。【客戶端】指的是,網(wǎng)絡(luò)連接的發(fā)起方,作為網(wǎng)絡(luò)處理的請(qǐng)求方,向?qū)Χ苏?qǐng)求某種服務(wù)?!痉?wù)器】指的是,網(wǎng)絡(luò)連接的被動(dòng)連接方,一般它不能主動(dòng)連接別人,只能監(jiān)聽(tīng)客戶端的連接,待它收到客戶端的服務(wù)請(qǐng)求后,會(huì)對(duì)客戶端的服務(wù)請(qǐng)求做出響應(yīng);通常服務(wù)器的運(yùn)行模式是一個(gè)服務(wù)器可對(duì)應(yīng)N個(gè)客戶端。
? 在TCP socket 網(wǎng)絡(luò)編程中,客戶端的代碼邏輯一般是:
【 socket -> bind -> connect -> send -> recv -> close 】
socket:創(chuàng)建一個(gè)socket套接字,用于執(zhí)行此次網(wǎng)絡(luò)連接
bind:將服務(wù)器的信息(主要是ip和端口)與創(chuàng)建的socket綁定
connect: 向服務(wù)器發(fā)起網(wǎng)絡(luò)連接請(qǐng)求
send: 將客戶端的數(shù)據(jù)發(fā)送到服務(wù)器端
recv: 接收服務(wù)器回應(yīng)的處理數(shù)據(jù)
close: 關(guān)閉socket套接字,釋放對(duì)應(yīng)的系統(tǒng)資源
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來(lái)直接上傳(img-6R5slz09-1661923478821)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]
? 對(duì)應(yīng)的,TCP服務(wù)器的代碼邏輯一般是:
【 socket -> bind -> listen -> accept -> recv -> send -> close 】
socket:創(chuàng)建一個(gè)socket套接字,用于執(zhí)行此次服務(wù)器的網(wǎng)絡(luò)服務(wù)
bind:將當(dāng)前需要?jiǎng)?chuàng)建的服務(wù)器的信息(主要是ip和端口)與創(chuàng)建的socket綁定,該ip和端口就是客戶端bind操作時(shí)需要用到的ip和端口
listen: 設(shè)置socket套接字執(zhí)行監(jiān)聽(tīng),此處可以設(shè)置服務(wù)器最多能同時(shí)接收多少個(gè)客戶端的連接
accept: 接受客戶端的連接請(qǐng)求,此處對(duì)應(yīng)的就是客戶端的connect操作
recv: 接收客戶端發(fā)送的請(qǐng)求數(shù)據(jù)
send: 將處理完的請(qǐng)求數(shù)據(jù)發(fā)送到客戶端
close: 關(guān)閉socket套接字,釋放對(duì)應(yīng)的系統(tǒng)資源
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來(lái)直接上傳(img-erGqy5UU-1661923478827)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]
? 了解了TCP客戶端和服務(wù)器的基本代碼邏輯后,我們直接附上tcp-echo-服務(wù)器的測(cè)試代碼:tcp_server_echo.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MAX_CLINET_NUM 10 /** 最大客戶端連接數(shù),可根據(jù)實(shí)際情況增減 */
/** 使用hexdump格式打印數(shù)據(jù)的利器 */
static void hexdump(const char *title, const void *data, unsigned int len)
{
char str[160], octet[10];
int ofs, i, k, d;
const unsigned char *buf = (const unsigned char *)data;
const char dimm[] = "+------------------------------------------------------------------------------+";
printf("%s (%d bytes)\n", title, len);
printf("%s\r\n", dimm);
printf("| Offset : 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 0123456789ABCDEF |\r\n");
printf("%s\r\n", dimm);
for (ofs = 0; ofs < (int)len; ofs += 16) {
d = snprintf( str, sizeof(str), "| %08x: ", ofs );
for (i = 0; i < 16; i++) {
if ((i + ofs) < (int)len)
snprintf( octet, sizeof(octet), "%02x ", buf[ofs + i] );
else
snprintf( octet, sizeof(octet), " " );
d += snprintf( &str[d], sizeof(str) - d, "%s", octet );
}
d += snprintf( &str[d], sizeof(str) - d, " " );
k = d;
for (i = 0; i < 16; i++) {
if ((i + ofs) < (int)len)
str[k++] = (0x20 <= (buf[ofs + i]) && (buf[ofs + i]) <= 0x7E) ? buf[ofs + i] : '.';
else
str[k++] = ' ';
}
str[k] = '\0';
printf("%s |\r\n", str);
}
printf("%s\r\n", dimm);
}
/** 獲取客戶端的ip和端口信息 */
static int get_clinet_ip_port(int sock, char *ip_port, int len, int *port)
{
struct sockaddr_in sa;
int sa_len;
sa_len = sizeof(sa);
if(!getpeername(sock, (struct sockaddr *)&sa, &sa_len)) {
*port = ntohs(sa.sin_port);
snprintf(ip_port, len, "%s", inet_ntoa(sa.sin_addr));
}
return 0;
}
/** 服務(wù)器端處理客戶端請(qǐng)求數(shù)據(jù)的線程入口函數(shù) */
static void *client_deal_func(void* arg)
{
nt client_sock = *(int *)arg;
while(1) {
char buf[4096];
int ret;
memset(buf,'\0',sizeof(buf));
ret = read(client_sock,buf,sizeof(buf)); /* 讀取客戶端發(fā)送的請(qǐng)求數(shù)據(jù) */
if (ret <= 0) {
break; /* 接收出錯(cuò),跳出循環(huán) */
}
hexdump("server recv:", buf, ret);
ret = write(client_sock, buf, ret); /* 將收到的客戶端請(qǐng)求數(shù)據(jù)發(fā)送回客戶端,實(shí)現(xiàn)echo的功能 */
if( ret < 0) {
break; /* 發(fā)送出錯(cuò),跳出循環(huán) */
}
}
close(client_sock);
}
/** 服務(wù)器主函數(shù)入口,接受命令參數(shù)輸入,指定服務(wù)器監(jiān)聽(tīng)的端口號(hào) */
int main(int argc, char **argv)
{
int ret;
int ser_port = 0;
int ser_sock = -1;
int client_sock = -1;
struct sockaddr_in server_socket;
struct sockaddr_in socket_in;
pthread_t thread_id;
int val = 1;
/* 命令行參數(shù)的簡(jiǎn)單判斷和help提示 */
if(argc != 2) {
printf("usage: ./client [port]\n");
ret = -1;
goto exit_entry;
}
/* 讀取命令行輸入的服務(wù)器監(jiān)聽(tīng)的端口 */
ser_port = atoi(argv[1]);
if (ser_port <=0 || ser_port >= 65536) {
printf("server port error: %d\n", ser_port);
ret = -2;
goto exit_entry;
}
/* 創(chuàng)建socket套接字 */
ser_sock = socket(AF_INET, SOCK_STREAM, 0);
if(ser_sock < 0) {
perror("socket error");
return -3;
}
/* 設(shè)置socket屬性,使得服務(wù)器使用的端口,釋放后,別的進(jìn)程立即可重復(fù)使用該端口 */
ret = setsockopt(ser_sock, SOL_SOCKET,SO_REUSEADDR, (void *)&val, sizeof(val));
if(ret == -1) {
perror("setsockopt");
return -4;
}
bzero(&server_socket, sizeof(server_socket));
server_socket.sin_family = AF_INET;
server_socket.sin_addr.s_addr = htonl(INADDR_ANY); //表示本機(jī)的任意ip地址都處于監(jiān)聽(tīng)
server_socket.sin_port = htons(ser_port);
/* 綁定服務(wù)器信息 */
if(bind(ser_sock, (struct sockaddr*)&server_socket, sizeof(struct sockaddr_in)) < 0) {
perror("bind error");
ret = -5;
goto exit_entry;
}
/* 設(shè)置服務(wù)器監(jiān)聽(tīng)客戶端的最大數(shù)目 */
if(listen(ser_sock, MAX_CLINET_NUM) < 0) {
perror("listen error");
ret = -6;
goto exit_entry;
}
printf("TCP server create success, accepting clients ...\n");
for(;;) { /* 循環(huán)等待客戶端的連接 */
char buf_ip[INET_ADDRSTRLEN];
socklen_t len = 0;
client_sock = accept(ser_sock, (struct sockaddr*)&socket_in, &len);
if(client_sock < 0) {
perror("accept error");
ret = -7;
continue;
}
{
char client_ip[128];
int client_port;
get_clinet_ip_port(client_sock, client_ip, sizeof(client_ip), &client_port);
/* 打印客戶端的ip和端口信息 */
printf("client connected [ip: %s, port :%d]\n", client_ip, client_port);
}
/* 使用多線程的方式處理客戶端的請(qǐng)求,每接收一個(gè)客戶端連接,啟動(dòng)一個(gè)線程處理對(duì)應(yīng)的數(shù)據(jù) */
pthread_create(&thread_id, NULL, (void *)client_deal_func, (void *)&client_sock);
pthread_detach(thread_id);
}
exit_entry:
if (ser_sock >= 0) {
close(ser_sock); /* 程序退出前,釋放socket資源 */
}
return 0;
}
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來(lái)直接上傳(img-PtxtqEBB-1661923478828)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]
TCP服務(wù)器端如何獲取客戶端的IP地址和端口信息
? 如上的測(cè)試代碼中,有這么一個(gè)函數(shù):
/** 獲取客戶端的ip和端口信息 */
static int get_clinet_ip_port(int sock, char *ip_port, int len, int *port)
{
struct sockaddr_in sa;
int sa_len;
sa_len = sizeof(sa);
if(!getpeername(sock, (struct sockaddr *)&sa, &sa_len)) {
*port = ntohs(sa.sin_port);
snprintf(ip_port, len, "%s", inet_ntoa(sa.sin_addr));
}
return 0;
}
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來(lái)直接上傳(img-s92JFClt-1661923478839)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]
? get_clinet_ip_port函數(shù)是在服務(wù)器成功接受了客戶端的連接之后被調(diào)用,sock是該通訊鏈路對(duì)應(yīng)的socket通道,函數(shù)內(nèi)部通過(guò)getpeername接口,取得對(duì)方(客戶端)的地址信息,存放在結(jié)構(gòu)體sa中;接著使用ntohs將sa中的端口信息轉(zhuǎn)成int類(lèi)型,通過(guò)函數(shù)的入?yún)ort傳遞出去;使用inet_ntoa將sa中的ip地址信息轉(zhuǎn)成字符串類(lèi)型,通過(guò)函數(shù)的入?yún)p傳遞出去。這樣,函數(shù)的調(diào)用者,通過(guò)ip和port變量就取得了客戶端的ip和端口信息了。下面會(huì)給出,這個(gè)函數(shù)成功調(diào)用后,打印出的客戶端信息范例。
TCP回顯測(cè)試服務(wù)器的使用和驗(yàn)證
? 有了tcp-server-echo的代碼,我們就可以執(zhí)行編譯、測(cè)試了。編譯程序,在Linux控制臺(tái)如下輸入:
gcc tcp_server_echo.c -o tcp_server_echo -lpthread
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來(lái)直接上傳(img-RgCENij3-1661923478841)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]
? 加上-lpthread表示鏈接多線程庫(kù),因?yàn)槌绦蛑杏玫搅硕嗑€程操作。正常編譯成功后,就可以在當(dāng)前工程目錄看到tcp_server_echo文件的存在,這個(gè)就是我們編譯出來(lái)的可執(zhí)行文件。
? 編譯成功后,使用以下命令啟動(dòng)服務(wù)器,其中6210表示啟動(dòng)服務(wù)器需要監(jiān)聽(tīng)的端口號(hào);注意,啟動(dòng)服務(wù)器時(shí)一定要輸入監(jiān)聽(tīng)的端口號(hào),否則啟動(dòng)會(huì)報(bào)錯(cuò)。
./tcp_server_echo 6210
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來(lái)直接上傳(img-RpaaPcOC-1661923478843)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]
? 以下是筆者使用該測(cè)試服務(wù)器對(duì)客戶端的連接做echo測(cè)試,記錄如下:
? 服務(wù)器端的輸出:

[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來(lái)直接上傳(img-3rgsFgSy-1661923478845)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]編輯
? 以下是客戶端對(duì)應(yīng)的接收的3組echo請(qǐng)求數(shù)據(jù):

[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來(lái)直接上傳(img-JZv1RaVq-1661923478849)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]編輯

[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來(lái)直接上傳(img-k63YVfFw-1661923478850)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]編輯

[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來(lái)直接上傳(img-cVqFINIm-1661923478852)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]編輯
? 經(jīng)對(duì)比可以發(fā)現(xiàn),echo的數(shù)據(jù)與客戶端發(fā)送的原始請(qǐng)求數(shù)據(jù)是一致的,證明echo-server運(yùn)行是完全沒(méi)有問(wèn)題的。
? 綜述,靈活使用好這個(gè)echo服務(wù)器可以高效地對(duì)客戶端的網(wǎng)絡(luò)做一些排查工作,比如通過(guò)客戶端去連接這個(gè)echo服務(wù)器,就可以很快知道客戶端當(dāng)前的網(wǎng)絡(luò)環(huán)境是不是暢通的?數(shù)據(jù)發(fā)送和數(shù)據(jù)接收功能是否是正常的?還可以大致分析出客戶端網(wǎng)絡(luò)通訊的瓶頸,究竟是連接耗時(shí)還是數(shù)據(jù)發(fā)送耗時(shí),還是數(shù)據(jù)接收耗時(shí),具體的耗時(shí)大致是什么級(jí)別,等等。
? 話又說(shuō)回來(lái),文中的echo服務(wù)器代碼畢竟僅僅是測(cè)試代碼,僅用于應(yīng)對(duì)一些網(wǎng)絡(luò)測(cè)試功能;如果真要應(yīng)用在正式的生產(chǎn)環(huán)境,那其中的個(gè)別代碼還需要進(jìn)一步斟酌、優(yōu)化,這部分的工作就交給有心的讀者吧。如果讀者在閱讀文本的過(guò)程中,發(fā)現(xiàn)有紕漏之處,可以隨時(shí)與筆者聯(lián)系,歡迎您的指正。謝謝。
-
服務(wù)器
+關(guān)注
關(guān)注
13文章
9795瀏覽量
88001 -
TCP
+關(guān)注
關(guān)注
8文章
1402瀏覽量
81054 -
ECHO
+關(guān)注
關(guān)注
1文章
73瀏覽量
27760 -
網(wǎng)絡(luò)編程
+關(guān)注
關(guān)注
0文章
72瀏覽量
10631
發(fā)布評(píng)論請(qǐng)先 登錄
串口服務(wù)器——TCP Server

三分鐘使用HMI Board完成TCP ECHO服務(wù)器的搭建

測(cè)試echo服務(wù)器lwip時(shí)出現(xiàn)問(wèn)題的解決辦法?
如何使用Socket實(shí)現(xiàn)TCP服務(wù)器?
4412開(kāi)發(fā)板Qt網(wǎng)絡(luò)編程-TCP實(shí)現(xiàn)服務(wù)器和客戶端
TCP服務(wù)器創(chuàng)建過(guò)程
使用網(wǎng)絡(luò)調(diào)試助手模擬TCP服務(wù)器
echo什么意思_@echo off的作用

網(wǎng)絡(luò)調(diào)試和串口調(diào)試集合UDP TCP客戶端和TCP服務(wù)器端應(yīng)用程序免費(fèi)下載

ESP8266作為TCP客戶端連接TCP服務(wù)器和測(cè)試的實(shí)例資料說(shuō)明

Linux下網(wǎng)絡(luò)編程TCP并發(fā)服務(wù)器和TCP客戶端程序免費(fèi)下載

Linux下TCP網(wǎng)絡(luò)編程-創(chuàng)建服務(wù)器與客戶端

基于LwIP的TCP服務(wù)器設(shè)計(jì)

基于TCP的Telnet服務(wù)器設(shè)計(jì)

評(píng)論