Linux網路程式設計:TCP客戶/伺服器模型及基本socket函式
TCP客戶/伺服器模型
TCP連線的分組交換
在使用socket API的時候應該清楚應用程式和TCP協議棧是如何互動的:
呼叫connect()會發出SYN段(SYN是TCP報文段頭部的一個標誌位,置為1)
阻塞的read()函式返回0就表明收到了FIN段
客戶端呼叫connect()發起連線,伺服器端accept()返回套接字的時候就意味著TCP的三次握手已經完成
TCP11種狀態:除了圖中的10種狀態之外,還有一種狀態叫CLOSING,產生的原因是雙方同時關閉。
socket函式
建立一個套接字用於通訊
#include <sys/types.h> #include <sys/socket.h> int socket(int domain, int type, int protocol);
引數:
domain:指定通訊協議族(protocol family),常用取值AF_INET(IPv4)
type:指定socket型別, 流式套接字SOCK_STREAM,資料報套接字SOCK_DGRAM,原始套接字SOCK_RAW
protocol:協議型別,常用取值0, 使用預設協議
返回值: 成功: 返回非負整數,套接字; 失敗: 返回-1
bind函式
繫結一個本地地址到套接字
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
引數:
sockfd:socket函式返回的套接字
addr:要繫結的地址
listen函式
listen函式應該用在呼叫socket和bind函式之後, 並且用在呼叫accept之前, 用於將一個套接字從一個主動套接字轉變成為被動套接字。
int listen(int sockfd, int backlog);
引數backlog說明:
對於給定的監聽套介面,核心要維護兩個佇列:
1、已由客戶發出併到達伺服器,伺服器正在等待完成相應的TCP三路握手過程(SYN_RCVD狀態)
2、已完成連線的佇列(ESTABLISHED狀態)
但是兩個佇列長度之和不能超過backlog。(具體內容在UNP書裡有)
accept函式:
從已完成連線佇列返回第一個連線,如果已完成連線佇列為空,則阻塞。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
引數:
sockfd:伺服器套接字
addr:將返回對等方的套接字地址, 不關心的話, 可以設定為NULL
addrlen:返回對等方的套接字地址長度, 不關心的話可以設定成為NULL, 否則一定要初始化
返回值: 成功則返回非負描述符,出錯則返回-1
connect函式
建立一個連線至addr所指定的套接字
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
引數:
sockfd:未連線套接字
addr:要連線的套接字地址
addrlen:第二個引數addr長度
簡單的回射客戶/伺服器模型
簡單的回射客戶/伺服器例子
//echosrv.c
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main(void)
{
//建立一個套接字
int listenfd;
if((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTP_TCP)) < 0)
ERR_EXIT("socket");
//初始化地址
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(5188);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
//在bind之前開啟“地址重複利用”(設定套接字選項SO_REUSEADDR避免TIME_WAIT狀態的出現)
int on = 1;
if(setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
ERR_EXIT("setsockopt");
//繫結
if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
ERR_EXIT("bind");
//監聽
if (listen(listenfd, SOMAXCONN) < 0)
ERR_EXIT("listen");
struct sockaddr_in peeraddr;//對方地址
socklen_t peerlen = sizeof(peeraddr);
int conn;//已連線套接字
//接受連線
if ((conn = accept(listenfd, (struct sockaddr *)&peeraddr, &peerlen)) < 0)
ERR_EXIT("accept");
//連線成功後打印出對方的地址
printf("客戶端的ip = %s, port = %d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
//處理通訊細節
char recvbuf[1024];
while (1)
{
memset(recvbuf, 0, sizeof(recvbuf));
int ret = read(conn, recvbuf, sizeof(recvbuf));
fputs(recvbuf, stdout);
write(conn, recvbuf, ret);
}
//關閉套接字
close(conn);
close(listenfd);
return 0;
}
//echocli.c
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main(void)
{
//建立套接字
int sock;
if((sock = socket(PF_INET, SOCK_STREAM, 0)) < 0)
ERR_EXIT("socket");
//初始化所要連線的對方地址
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(5188);
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
//連線
if (connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr) ) < 0)
ERR_EXIT("connect");
char sendbuf[1024] = {0};
char recvbuf[1024] = {0};
//通訊細節
while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
write(sock, sendbuf, strlen(sendbuf));
read(sock, recvbuf, strlen(recvbuf));
fputs(recvbuf, stdout);
memset(sendbuf, 0, sizeof(sendbuf));
memset(recvbuf, 0, sizeof(recvbuf));
}
close(sock);
return 0;
}