day33練習:
客戶端 與 服務器實現一個點對點聊天tcp
客戶端
clifd = socket
connect
//收 --父進程 //發 --子進程 tcp
服務器 listenfd = socket
bind
listen
connfd = accept()
//收 -- 父進程 //發 -- 子進程
client.c
#include "../head.h"int res_fd[1]; // 只需要存儲客戶端socket文件描述符int tcp_client_connect(char const *ip,char const * port)
{int fd = socket(AF_INET,SOCK_STREAM,0);if (fd < 0){perror("socket fail");return -1;}res_fd[0] = fd; // 保存文件描述符用于清理printf("fd = %d\n",fd);struct sockaddr_in seraddr;//ip 192.168.0.150//port 50000bzero(&seraddr,sizeof(seraddr)); // 使用bzero清零seraddr.sin_family = AF_INET;seraddr.sin_port = htons(atoi(port)); //atoiseraddr.sin_addr.s_addr = inet_addr(ip);if ( connect(fd,(const struct sockaddr *)&seraddr,sizeof(seraddr)) < 0){perror("connect fail");return -1;}return fd;
}// 信號處理函數
void do_handler(int signo)
{exit(0);
}// 清理函數
void cleanup(void)
{close(res_fd[0]);printf("pid = %d ---cleanup ---exit---\n",getpid());
}//./cli 192.168.0.130 50000
int main(int argc, char const *argv[])
{if (argc != 3){printf("Usage: %s <ip> <port>\n",argv[0]); return -1; }int clifd = tcp_client_connect(argv[1],argv[2]);if (clifd < 0){printf("tcp_client_connect fail\n");return -1;}// 注冊信號處理函數signal(SIGUSR1,do_handler);pid_t pid = fork();if (pid < 0){perror("fork fail");return -1;}// 注冊退出清理函數if (atexit(cleanup) != 0){perror("atexit fail");return -1;}char buf[1024];if (pid > 0){printf("---f -- pid = %d\n",getpid());while (1){printf(">");fgets(buf,sizeof(buf),stdin);write(clifd,buf,strlen(buf)+1);if (strncmp(buf,"quit",4) == 0){kill(pid,SIGKILL);wait(NULL);exit(0);}}}else if (pid == 0){printf("---c -- pid = %d\n",getpid());while (1){read(clifd,buf,sizeof(buf));if (strncmp(buf,"quit",4) == 0){//kill(getppid(),SIGKILL);kill(getppid(),SIGUSR1); // 使用SIGUSR1信號通知父進程退出exit(0);}printf("cli buf: %s\n",buf);}}// 程序不會執行到這里,但為了完整性保留close(clifd);return 0;
}
sever.c
#include "head.h"int res_fd[2];int tcp_accept(char const*ip,char const * port )
{//step1 socket int fd = socket(AF_INET,SOCK_STREAM,0);if (fd < 0){perror("socket fail");return -1;}res_fd[0] = fd;printf("fd = %d\n",fd);struct sockaddr_in seraddr;//ip 192.168.0.150//port 50000bzero(&seraddr,sizeof(seraddr));seraddr.sin_family = AF_INET;seraddr.sin_port = htons(atoi(port));seraddr.sin_addr.s_addr = inet_addr(ip);if (bind(fd,(const struct sockaddr *)&seraddr,sizeof(seraddr)) < 0){perror("connect fail");return -1;}if (listen(fd,5) < 0){perror("listen fail");return -1;}int connfd = accept(fd,NULL,NULL);if (connfd < 0){perror("accept fail");return -1;}res_fd[1] = connfd;return connfd;
}void do_handler(int signo)
{exit(0);
}void cleanup(void)
{close(res_fd[0]);close(res_fd[1]);printf("pid = %d ---cleanup ---exit---\n",getpid());
}int main(int argc, char const *argv[])
{if (argc != 3){printf("Usage: %s <ip> <port>\n",argv[0]);return -1;}int connfd = tcp_accept(argv[1],argv[2]);if (connfd < 0){printf("tcp_accept fail");return -1;}signal(SIGUSR1,do_handler);pid_t pid = fork();if (pid < 0){perror("fork fail");return -1;}//退出資源的管理 if (atexit(cleanup) != 0) // 注冊 一個 清理函數 {perror("atexit fail");return -1;}char buf[1024];if (pid > 0){printf("---f -- pid = %d\n",getpid());while (1){printf(">");fgets(buf,sizeof(buf),stdin);write(connfd,buf,strlen(buf)+1);if (strncmp(buf,"quit",4) == 0){kill(pid,SIGKILL);wait(NULL);exit(0);}}}else if (pid == 0){printf("---c -- pid = %d\n",getpid());while (1){read(connfd,buf,sizeof(buf));if (strncmp(buf,"quit",4) == 0){//kill(getppid(),SIGKILL);kill(getppid(),SIGUSR1);exit(0);}printf("cli buf: %s\n",buf);}}close(connfd);return 0;
}
1. 什么是粘包?
粘包(TCP 粘包問題)是指 發送方 分多次發送的數據包,在 接收方 接收到時,數據會被“粘”在一起,導致接收方無法正確區分數據包的邊界。
2. 粘包的原因
粘包問題本質上是因為 TCP 是面向流的協議,它沒有明確的消息邊界。具體原因如下:
1. TCP 是流式協議
TCP 協議把數據看作是一條字節流,不關心應用層的數據是否被完整地發送或接收,接收方只能收到一個“字節流”。
所以,發送方分多次發送的數據可能會被合并在一起發送,也可能是一次發送的數據被拆分成多個小塊接收。
2. 操作系統的緩沖機制
TCP 發送方的數據會存入發送緩沖區,直到緩沖區數據達到一定量或操作系統覺得合適時,數據才會被發送出去。
同樣,接收方的操作系統會將收到的網絡數據包緩存到接收緩沖區,等待應用程序讀取。
3. 粘包與拆包
除了粘包,另一個常見的問題是 拆包。拆包是指應用程序發送的數據包在傳輸過程中被拆成多個小包,導致接收方無法正確地拼接成原本的消息。
4. 解決粘包問題
為了避免粘包和拆包的問題,應用層需要通過某種機制來明確 數據包的邊界。常見的解決方法有以下幾種:
1. 固定長度的消息
每個發送的消息都固定長度。接收方一接收到數據,就知道消息的長度。
例如,假設我們每次發送 10 字節 的消息,接收方就可以按照 10 字節 來讀取數據,保證數據不被粘連。
缺點:如果數據量小于固定長度,可能會浪費空間;如果數據量大于固定長度,則需要分段處理。
2. 包頭加包體方式
通過在每個消息前加一個 固定長度的頭部,頭部指定消息的 長度。接收方根據頭部的長度信息來確定如何讀取數據。
例如,發送一個消息時,先發送一個 4 字節的包頭,告訴接收方后續數據的長度。
優點:通過包頭可以解決粘包問題,靈活性高。
缺點:需要處理包頭和包體的分離。
3. 特定的分隔符
采用特定的分隔符(例如
\n
、##
等)來標識消息的邊界。接收方根據分隔符來區分不同的數據包。
優點:實現簡單,適用于文本數據。
缺點:對于二進制數據不太適用,容易導致數據誤解析。
4. 使用高級協議
一些協議(如 HTTP、WebSocket、MQTT)已經為數據包邊界做了設計,使用這些協議時,粘包問題會被協議本身解決。
練習:
client.c
#include "../head.h"int tcp_client_connect(char const *ip,char const * port)
{int fd = socket(AF_INET,SOCK_STREAM,0);if (fd < 0){perror("socket fail");return -1;}//printf("fd = %d\n",fd);struct sockaddr_in seraddr;//ip 192.168.0.150//port 50000seraddr.sin_family = AF_INET;seraddr.sin_port = htons(atoi(port)); //atoiseraddr.sin_addr.s_addr = inet_addr(ip);if ( connect(fd,(const struct sockaddr *)&seraddr,sizeof(seraddr)) < 0){perror("connect fail");return -1;}return fd;
}//./cli 192.168.0.130 50000
int main(int argc, char const *argv[])
{if (argc != 3){printf("Usage: %s <ip> <port>\n",argv[0]); return -1; }int clifd = tcp_client_connect(argv[1],argv[2]);if (clifd < 0){printf("tcp_client_connect fail\n");return -1;}msg_t msg;printf("Input file name:");msg.type = -1;fgets(msg.buf,sizeof(msg.buf),stdin);msg.buf[strlen(msg.buf)-1] = '\0';write(clifd,&msg,sizeof(msg));int fd_s = open(msg.buf,O_RDONLY);if (fd_s < 0){perror("open fail");return -1;}while (1){int ret = read(fd_s,msg.buf,sizeof(msg.buf));msg.type = ret;write(clifd,&msg,sizeof(msg));if (ret == 0)break;}close(fd_s);close(clifd);return 0;
}
server.c
#include "../head.h"int tcp_accept(char const*ip,char const * port )
{//step1 socket int fd = socket(AF_INET,SOCK_STREAM,0);if (fd < 0){perror("socket fail");return -1;}printf("fd = %d\n",fd);struct sockaddr_in seraddr;//ip 192.168.0.150//port 50000bzero(&seraddr,sizeof(seraddr));seraddr.sin_family = AF_INET;seraddr.sin_port = htons(atoi(port));seraddr.sin_addr.s_addr = inet_addr(ip);if (bind(fd,(const struct sockaddr *)&seraddr,sizeof(seraddr)) < 0){perror("connect fail");return -1;}if (listen(fd,5) < 0){perror("listen fail");return -1;}int connfd = accept(fd,NULL,NULL);if (connfd < 0){perror("accept fail");return -1;}return connfd;
}int main(int argc, char const *argv[])
{if (argc != 3){printf("Usage: %s <ip> <port>\n",argv[0]);return -1;}int connfd = tcp_accept(argv[1],argv[2]);if (connfd < 0){printf("tcp_accept fail");return -1;}msg_t msg;read(connfd,&msg,sizeof(msg));printf("buf = %s\n",msg.buf);int fd_d = open(msg.buf,O_WRONLY|O_TRUNC|O_CREAT,0666);if (fd_d < 0){perror("open fail");return -1;}while (1){int ret = read(connfd,&msg,sizeof(msg));if (msg.type == 0)break;write(fd_d,msg.buf,msg.type);}close(connfd);close(fd_d);return 0;
}
recv
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
參數說明
sockfd
要接收數據的套接字描述符。它是通過
socket()
創建,并且已經通過connect()
或accept()
建立連接的套接字。
buf
用于接收數據的緩沖區(指針)。接收到的數據會存放在這個緩沖區中。
len
緩沖區的大小,即最多接收的字節數。
flags
控制選項,一般使用
0
,表示沒有特殊的控制。也可以使用一些標志,例如:MSG_PEEK
:查看數據但不從緩沖區移除。MSG_DONTWAIT
:非阻塞模式下調用。
返回值
成功:返回實際接收到的字節數。
如果返回
0
,表示對方已經關閉連接。如果返回大于
0
,表示接收到的數據的字節數。
失敗:返回
-1
,并設置errno
。常見錯誤包括:EAGAIN
/EWOULDBLOCK
:非阻塞模式下,接收緩沖區沒有數據。ECONNRESET
:連接被對方重置。
recv()
與 read()
的區別
在 Linux 中,recv()
和 read()
函數在套接字上基本上是等價的,都會從套接字中讀取數據。但是 recv()
提供了一些額外的功能:
recv()
可以使用flags
參數,控制接收操作的行為。recv()
更常用于網絡編程,因為它可以在接收數據時設置額外的標志(比如非阻塞接收、查看數據等)。
send
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
參數說明
sockfd
發送數據的套接字描述符,通常是通過
socket()
創建并且已經連接(使用connect()
或accept()
)的套接字。
buf
發送數據的緩沖區(指向內存的指針)。你希望通過
send()
發送的數據應該存放在這個緩沖區中。
len
要發送的字節數(數據的長度)。
len
不能超過緩沖區buf
的大小。
flags
控制發送行為的標志,通常設置為
0
,但也可以設置為一些特定的標志,例如:MSG_DONTWAIT
:非阻塞模式,立即返回(如果不能發送數據)。MSG_NOSIGNAL
:不觸發SIGPIPE
信號(通常用在當對方關閉連接時)。
返回值
成功:返回實際發送的字節數(即已寫入套接字的字節數)。這個值可能小于你請求發送的字節數(即發送過程可能被中斷,部分數據發送成功)。你需要處理這種情況,確保數據完全發送。
失敗:返回
-1
,并設置errno
。常見的錯誤包括:EAGAIN
/EWOULDBLOCK
:非阻塞模式下,緩沖區沒有空間或者網絡忙。ECONNRESET
:連接被對方重置。
send()
與 write()
的區別
在 Linux 中,send()
和 write()
在行為上是非常相似的。兩者都可以用于套接字,發送數據時的行為幾乎一致。區別在于:
send()
函數支持flags
參數,可以控制發送行為(例如是否阻塞、是否觸發SIGPIPE
等)。write()
通常用于文件描述符,不具有像send()
那樣的靈活控制。
所以,send()
更常用于網絡編程,因為它具有更高的控制靈活性。
udb編程
UDP(User Datagram Protocol,用戶數據報協議) 是一個 無連接 的傳輸層協議,它是 TCP 的一個對立面。與 TCP 不同,UDP 不保證數據的可靠性和順序,它簡單、快速、適用于對速度要求高且能容忍一定丟包的場景。
特點:
無連接:UDP 不需要建立連接,發送數據時不進行握手,發送方直接將數據發送給接收方。
不保證順序:接收方可能會收到亂序的包,也可能丟失一些包。
輕量級:UDP 頭部開銷小,適用于對實時性要求較高的應用(例如視頻、語音等流媒體)。
無重傳機制:如果數據丟失,UDP 不會重傳丟失的部分,應用層需要自行處理丟包。
面向數據報:每次發送的數據都是一個獨立的包,且有明確的邊界。
sendto
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
參數說明
sockfd
類型:
int
描述:由
socket()
函數創建的套接字描述符。用途:該套接字用于指定數據發送的目標套接字。對于 UDP,通常是使用
SOCK_DGRAM
類型的套接字。
buf
類型:
const void *
描述:一個指向數據緩沖區的指針,這個緩沖區包含了要發送的數據。
用途:將要發送的消息數據存放在
buf
中,可以是任何類型的數據(通常是字符串或二進制數據)。
len
類型:
size_t
描述:緩沖區中要發送的數據的字節數。
用途:指定從
buf
中發送的字節數。例如,如果buf
是一個字符串,len
就是字符串的長度。
flags
類型:
int
描述:控制發送操作的標志位。
用途:通常設為
0
,但可以使用特定的標志:MSG_DONTWAIT
:非阻塞模式。如果套接字的緩沖區沒有足夠的空間,sendto()
會立即返回,而不是阻塞。MSG_NOSIGNAL
:防止發送數據時觸發SIGPIPE
信號(通常在連接被關閉時)。
dest_addr
類型:
const struct sockaddr *
描述:指向目標地址的指針。
用途:指定接收數據的目標地址,通常是一個
struct sockaddr_in
(IPv4 地址)或者struct sockaddr_in6
(IPv6 地址)結構體。
addrlen
類型:
socklen_t
描述:目標地址結構體的長度。
用途:指定
dest_addr
的長度,通常使用sizeof(struct sockaddr_in)
或sizeof(struct sockaddr_in6)
來獲取。
返回值
成功:返回實際發送的字節數(即
len
字節)。失敗:返回
-1
,并設置errno
。常見的錯誤包括:EAGAIN
/EWOULDBLOCK
:非阻塞模式下,緩沖區沒有足夠的空間。ENOTCONN
:套接字未連接(對于某些協議需要連接)。
recvfrom
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
參數說明
sockfd
類型:
int
描述:由
socket()
創建的 UDP 套接字描述符。用途:用于指定接收數據的套接字。
buf
類型:
void *
描述:接收數據的緩沖區。
用途:存放從 UDP 套接字接收到的數據。這個緩沖區的大小應足夠存放可能接收到的最大數據。
len
類型:
size_t
描述:緩沖區的大小,即最多接收的字節數。
用途:告知
recvfrom()
函數最多接收多少字節數據。
flags
類型:
int
描述:接收的控制選項,通常設置為
0
,但可以使用一些標志,例如:MSG_PEEK
:查看數據但不從接收緩沖區移除數據。MSG_DONTWAIT
:非阻塞模式,立即返回(如果沒有數據可接收)。
src_addr
類型:
struct sockaddr *
描述:指向發送方地址的結構體指針。
用途:接收數據時,會將發送方的地址信息(如 IP 地址和端口)存放在這里。
通常使用
struct sockaddr_in
(IPv4)或struct sockaddr_in6
(IPv6)來表示。
addrlen
類型:
socklen_t *
描述:發送方地址結構體的長度,接收時會更新為實際的地址長度。
用途:傳遞給
recvfrom()
時,表示地址結構的大小,返回時更新為實際接收的數據源地址的長度。
返回值
成功:返回接收到的字節數。如果接收到的數據是完整的消息,
recvfrom()
將返回實際接收到的數據字節數。失敗:返回
-1
,并設置errno
。常見錯誤包括:EAGAIN
/EWOULDBLOCK
:非阻塞模式下,沒有數據可以接收。ECONNRESET
:連接被重置。