LinuxC 搭建簡單的TCP服務(wù)器
1. 問題
? 在標(biāo)題之前,先提幾個(gè)問題,方便下次查看理解。
- 什么是TCP
- TCP服務(wù)器需要用到哪些函數(shù)
- 如何簡單的搭建一個(gè)TCP服務(wù)器
2. 什么是TCP
? TCP 是一種傳輸層協(xié)議,可以提供可靠的數(shù)據(jù)傳輸服務(wù)。它是面向連接的,具有可靠性、流量控制、擁塞控制以及雙工通信的特點(diǎn)。
3. TCP 服務(wù)器需要用到哪些函數(shù)
1. socket
int socket(int domain, int type, int protocol); //聲明
int sockfd = socket(AF_INET, SOCK_STREAM, 0); //示例
? socket 作用是用來創(chuàng)建一個(gè)文件描述符也成為套接字描述符,用于根據(jù)我們指定的協(xié)議族、數(shù)據(jù)類型和協(xié)議來分配一個(gè)套接字描述符以及它所用到的資源。函數(shù)調(diào)用失敗返回-1,調(diào)用成功返回正整數(shù)。
? 參數(shù)說明:
-
domain
:指定協(xié)議族,常用的有AF_INET
(IPv4 地址)和AF_INET6
(IPv6 地址)、AF_LOCAL
、AF_ROUTE
等。 -
type
:指定套接字類型,有3種類型,常用的有SOCK_STREAM
(流式套接字,用于 TCP 協(xié)議)和SOCK_DGRAM
(數(shù)據(jù)報(bào)套接字,用于 UDP 協(xié)議)。第三種為SOCK_RAW
,為原始類型,允許對(duì)底層協(xié)議(如 IP, ICMP)進(jìn)行直接訪問。 -
protocol
:指定協(xié)議,通常設(shè)置為 0,表示讓系統(tǒng)根據(jù)domain
和type
自動(dòng)選擇合適的協(xié)議。
? 底層邏輯:
? socket()
函數(shù)的底層邏輯主要涉及創(chuàng)建一個(gè)套接字?jǐn)?shù)據(jù)結(jié)構(gòu),注冊(cè)到內(nèi)核中,為該套接字分配一個(gè)唯一的文件描述符,并返回該文件描述符。具體步驟如下:
- 創(chuàng)建套接字?jǐn)?shù)據(jù)結(jié)構(gòu):根據(jù)指定的通信域、套接字類型和協(xié)議,創(chuàng)建一個(gè)套接字?jǐn)?shù)據(jù)結(jié)構(gòu),用于表示一個(gè)通信端點(diǎn)。
- 分配文件描述符:在內(nèi)核中分配一個(gè)文件描述符,用于標(biāo)識(shí)這個(gè)套接字。
- 注冊(cè)到內(nèi)核中:將套接字?jǐn)?shù)據(jù)結(jié)構(gòu)注冊(cè)到內(nèi)核的套接字表中,以便內(nèi)核能夠識(shí)別和管理這個(gè)套接字。
- 返回文件描述符:將分配的文件描述符返回給調(diào)用者,以便后續(xù)對(duì)套接字的操作。
2. bind
// 聲明
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 示例
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(struct sockaddr_in));
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(2048);
if (-1 == bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr)))
{
perror("bind");
return -1;
}
? bind()
函數(shù)用于將一個(gè)本地地址(包括 IP 地址和端口號(hào))綁定到一個(gè)套接字上,以便后續(xù)對(duì)該套接字的操作可以與指定的地址相關(guān)聯(lián)。當(dāng)你調(diào)用 bind()
函數(shù)時(shí),你正在告訴操作系統(tǒng)將特定的 IP 地址和端口號(hào)綁定到一個(gè)套接字上。這樣做的目的是為了讓該套接字在網(wǎng)絡(luò)上可以被唯一標(biāo)識(shí),并且只有特定地址和端口號(hào)的數(shù)據(jù)才能發(fā)送到這個(gè)套接字上。
? 參數(shù)說明:
-
sockfd
:要綁定地址的套接字文件描述符。 -
addr
:指向包含要綁定地址的結(jié)構(gòu)體的指針,通常是struct sockaddr
類型的指針,需要根據(jù)套接字類型進(jìn)行類型轉(zhuǎn)換。 -
addrlen
:指定地址結(jié)構(gòu)體的長度。
3. listen
? listen()
函數(shù)用于將一個(gè)套接字描述符標(biāo)記為被動(dòng)套接字(socket()
函數(shù)創(chuàng)建的套接字為主動(dòng)屬性),用于監(jiān)聽連接請(qǐng)求,等待客戶端的連接。所以它的作用是設(shè)置套接字為監(jiān)聽狀態(tài),等待客戶端的連接請(qǐng)求。
// 聲明
int listen(int sockfd, int backlog);
// 示例
listen(sockfd, 10);
? 參數(shù)說明:
-
sockf
:要設(shè)置為監(jiān)聽狀態(tài)的套接字文件描述符。 -
backlog
:待連接隊(duì)列的最大長度,即允許等待連接的客戶端數(shù)量。
4. accept
// 聲明
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
? accept()
函數(shù)用于從已監(jiān)聽的的套接字中接收一個(gè)連接請(qǐng)求,創(chuàng)建一個(gè)新的連接套接字,并返回新的套接字連接符。如果已完成連接隊(duì)列為空,則線程進(jìn)入阻塞狀態(tài)。如果accept
函數(shù)執(zhí)行成功,則返回由內(nèi)核自動(dòng)生成的套接字描述符,表示服務(wù)器已與客戶端已經(jīng)建立連接;若執(zhí)行錯(cuò)誤則返回-1。
? 參數(shù)說明:
-
sockfd
:已監(jiān)聽的套接字描述符(由socket()
函數(shù)返回的套接字描述符) -
addr
:一個(gè) sockaddr 的結(jié)構(gòu)體,用于存放發(fā)起連接請(qǐng)求的客戶端協(xié)議地址 -
addrlen
:指向存放客戶端地址信息結(jié)構(gòu)體長度的指針。
5. recv
// 聲明
int recv(int sockfd, char * buf, int len, int flags);
// 示例
char buffer[128] = {0};
int recv_count = recv(clientfd, buffer, 128, 0);
? recv()
函數(shù)用于接收已連接套接字發(fā)送的數(shù)據(jù)。從接收緩沖區(qū)中復(fù)制數(shù)據(jù),如果成功則返回復(fù)制的字節(jié)數(shù),失敗返回-1,對(duì)端斷開連接返回0。
? 參數(shù)說明:
-
sockfd
:要接收數(shù)據(jù)的套接字描述符 -
buf
:指向存放接收數(shù)據(jù)緩沖區(qū)的指針 -
len
:接收緩沖區(qū)的長度 -
flags
:接受操作的標(biāo)志位,通常為0,可以通過 ‘|’操作符連接到一起
6. send
// 聲明
int send(int sockfd, const void* buf, int len, int flags)
// 示例
char buffer[128] = {0};
int recv_count = recv(clientfd, buffer, 128, 0);
if (recv_count == 0)
{
close(clientfd);
}
send(clientfd, buffer, recv_count, 0);
? send()
函數(shù)用于向已連接的套接字發(fā)送數(shù)據(jù),每個(gè)TCP連接的套接字都有一個(gè)發(fā)送緩沖區(qū)(buf 是存放數(shù)據(jù)的緩沖區(qū),不是發(fā)送緩沖區(qū)),調(diào)用send
函數(shù)的過程是內(nèi)核將用戶的數(shù)據(jù)復(fù)制至TCP套接字的發(fā)送緩沖區(qū)的過程。send
函數(shù)被調(diào)用時(shí),檢查TCP套接字中是否有發(fā)送數(shù)據(jù)。
? 如果沒有數(shù)據(jù)發(fā)送,則比較發(fā)送緩沖區(qū)長度和 send
函數(shù)發(fā)送數(shù)據(jù)的長度 len
,如果len
大于套接字緩沖區(qū)長度,則返回錯(cuò)誤-1;如果發(fā)送緩沖區(qū)的大小足夠大,將數(shù)據(jù)發(fā)送至TCP發(fā)送緩沖區(qū),send
函數(shù)將數(shù)據(jù)復(fù)制到發(fā)送緩沖區(qū)中。
? 如果有數(shù)據(jù)還未發(fā)送,則比較該緩沖區(qū)剩余空間和len
的大小,如果len
大于剩余空間,則一致等待,直到發(fā)送緩沖區(qū)中的數(shù)據(jù)發(fā)送完為止;如果len
小于發(fā)送緩沖區(qū)剩余空間的大小,則將發(fā)送的數(shù)據(jù)復(fù)制到該緩沖區(qū)中。
? send
函數(shù)發(fā)送成功時(shí),返回實(shí)際復(fù)制的字節(jié)數(shù),發(fā)送失敗時(shí),返回-1,另一端關(guān)閉連接時(shí),返回0。
? 參數(shù)說明:
-
sockfd
:發(fā)送端套接字描述符 -
buf
:待發(fā)送數(shù)據(jù)的緩沖區(qū) -
len
:待發(fā)送數(shù)據(jù)的字節(jié)長度 -
flags
:發(fā)送操作的標(biāo)志位
4. 如何搭建一個(gè)簡單的服務(wù)器
1. 創(chuàng)建服務(wù)器sockfd
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
? 利用 socket
函數(shù)創(chuàng)建一個(gè)套接字描述符,設(shè)置協(xié)議族,指定為TCP(SOCK_STREAM
)
2. 綁定客戶端IP和port
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(struct sockaddr_in));
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(2048);
if (-1 == bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr)))
{
perror("bind");
return -1;
}
? 利用bind
函數(shù)將sockfd
與IP和端口綁定,此處 IP 任意(INADDR_ANY
)端口為2048。
3. 將sockfd設(shè)置為監(jiān)聽模式
listen(sockfd, 10);
? listen
函數(shù)將客戶端設(shè)置為監(jiān)聽狀態(tài),監(jiān)聽客戶端連接,設(shè)置最大連接為10個(gè)。
4. 接受客戶端連接
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
? 利用accept
函數(shù)從已監(jiān)聽的的套接字中接收一個(gè)連接請(qǐng)求。返回客戶端套接字描述符clientfd
。
5. 通信
while (1)
{
char buffer[128] = {0};
int recv_count = recv(clientfd, buffer, 128, 0);
if (recv_count == 0)
{
break
}
send(clientfd, buffer, recv_count, 0);
}
close(clientfd);
? recv
函數(shù)實(shí)現(xiàn)接受客戶端信息,send
發(fā)送信息給客戶端。close
函數(shù)回收clientfd
文件資源
5. 實(shí)現(xiàn)并發(fā)
? 如果按照上述程序?qū)⒋a組合起來,實(shí)現(xiàn)的TCP服務(wù)器僅能與一臺(tái)客戶機(jī)進(jìn)行連接通信。為了實(shí)現(xiàn)并發(fā)功能,需要用到多線程的方法。
1. 線程回調(diào)函數(shù)
void* client_thread(void *arg)
{
int clientfd = *(int *)arg;
while (1)
{
char buffer[128] = {0};
int recv_count = recv(clientfd, buffer, 128, 0);
if (recv_count == 0)
{
break;
}
send(clientfd, buffer, recv_count, 0);
}
}
close(clientfd);
? 該函數(shù)為線程的回調(diào)函數(shù),函數(shù)輸入一個(gè)客戶端clientfd
2. 根據(jù)客戶端連接請(qǐng)求創(chuàng)建線程
while (1)
{
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
pthread_t tid;
pthread_create(&tid, NULL, client_thread, &clientfd);
}
? 每當(dāng)客戶端有新的連接請(qǐng)求時(shí),accept
都會(huì)返回一個(gè)新的clientfd
,然后pthread_create
創(chuàng)建一個(gè)新線程,在該線程中服務(wù)器與該客戶端進(jìn)行通信。
6. 總結(jié)
? TCP 是一種傳輸層協(xié)議,可以提供可靠的數(shù)據(jù)傳輸服務(wù)。
? 在TCP服務(wù)器中用到了六個(gè)函數(shù)socket
、bind
、listen
、accept
、recv
和send
。文章來源:http://www.zghlxwxcb.cn/news/detail-854356.html
? 實(shí)現(xiàn)TCP服務(wù)器僅需將上述六個(gè)函數(shù)按順序運(yùn)用即可。我們還利用多線程的方法實(shí)現(xiàn)了一定量的并發(fā),但是依舊存在一些問題,它是一線程一請(qǐng)求的形式,如果請(qǐng)求過多,服務(wù)器將會(huì)資源耗盡,因此無法實(shí)現(xiàn)大的并發(fā)量。想要實(shí)現(xiàn)超大并發(fā),可以使用IO多路復(fù)用的功能。此處不做介紹,請(qǐng)聽下回分解。文章來源地址http://www.zghlxwxcb.cn/news/detail-854356.html
到了這里,關(guān)于Linux socket 搭建TCP服務(wù)器(C語言)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!