day33练习:
客户端 与 服务器实现一个点对点聊天
tcp
客户端
clifd = socket
connect
//收 --父进程 //发 --子进程 tcp
服务器 listenfd = socket
bind
listen
connfd = accept()
//收 -- 父进程 //发 -- 子进程
client.c
cs
#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 50000
bzero(&seraddr,sizeof(seraddr)); // 使用bzero清零
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(atoi(port)); //atoi
seraddr.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
cs
#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 50000
bzero(&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
cs
#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 50000
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(atoi(port)); //atoi
seraddr.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
cs
#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 50000
bzero(&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
:连接被重置。
-