tcp 协议是⾯向连接的协议,在实现 tcp 客户端时,则需要先连接服务器,后⾯才能进⾏通讯。
函数头⽂件:
#include
#include
函数原型:int socket(int domain,int type,int protocol)
函数功能:创建套接字
函数参数:
domain: 协议族,如 AF_INTE (表示IPV4)
type : 套接字类型
SOCK_STREAM : 流式套接字, 传输层使⽤ tcp 协议
SOCK_DGRAM : 数据包套接字, 传输层使⽤ udp 协议
protocol : 协议, 可以填0
函数返回值
成功 : 返回 套接字⽂件描述符
失败 : 返回 -1,并设置 errno
函数头⽂件:
#include
#include
函数原型:int connect(int sockfd,const struct sockaddr *addr,socklen_t addrlen)
函数功能:发起对套接字的连接 (基于连接的协议进行)
函数参数:
sockfd : 套接字⽂件描述符
addr : 连接的套接字的地址结构对象的地址 (⼀般为服务器)
internet 协议族使⽤的 struct sockaddr_in 结构体,⼤⼩与通⽤ struct sockaddr 结构体⼀致
addrlen : 地址结构的⻓度
函数返回值:
成功 : 返回 0
失败 : 返回 -1,并设置 errno
示例: 编码实现客户端主程序,并连接服务器。
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, const char *argv[])
{
if(argc != 3)
{
fprintf(stderr, "Usage : %s .\n" , argv[0]);
exit(EXIT_FAILURE);
}
//1.创建套接字
int sfd = socket(AF_INET, SOCK_STREAM, 0);
if(sfd == -1){
perror("[ERROR] Fail to socket.");
exit(EXIT_FAILURE);
}
//2.填写协议与服务器网络信息
struct sockaddr_in svr_addr;
bzero(&svr_addr, sizeof(struct sockaddr_in)); //清零
svr_addr.sin_family = AF_INET;
svr_addr.sin_port = htons(atoi(argv[2]));
svr_addr.sin_addr.s_addr = inet_addr(argv[1]);
//3.连接服务器
int ret = connect(sfd, (const struct sockaddr *)&svr_addr, sizeof(struct sockaddr_in));
if(ret == -1){
perror("[ERROR] Fail to connect.");
exit(EXIT_FAILURE);
}
close(sfd);
return 0;
}
基于 socket 发送数据需要调⽤ send 函数, 下⾯是 send 函数的具体信息。
函数头⽂件:
#include
#include
函数原型:ssize_t send(int sockfd,const void *buf,size_t len,int flags)
函数功能:基于套接字(建⽴连接后)发送数据
函数参数:
sockfd : 套接字⽂件描述符
buf : 发送缓冲区的地址
len : 发送数据的⻓度
flags : 发送标志位
函数返回值:
成功 : 返回 成功发送的字节数
失败 : 返回 -1, 并设置 errno
示例:客户端发送数据给服务器
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, const char *argv[])
{
if(argc != 3)
{
fprintf(stderr, "Usage : %s .\n" , argv[0]);
exit(EXIT_FAILURE);
}
//1.创建套接字
int sfd = socket(AF_INET, SOCK_STREAM, 0);
if(sfd == -1){
perror("[ERROR] Fail to socket.");
exit(EXIT_FAILURE);
}
struct sockaddr_in svr_addr;
bzero(&svr_addr, sizeof(struct sockaddr_in)); //清零
svr_addr.sin_family = AF_INET;
svr_addr.sin_port = htons(atoi(argv[2]));
svr_addr.sin_addr.s_addr = inet_addr(argv[1]);
int ret = connect(sfd, (const struct sockaddr *)&svr_addr, sizeof(struct sockaddr_in));
if(ret == -1){
perror("[ERROR] Fail to connect.");
exit(EXIT_FAILURE);
}
//2.获取输入并发送
ssize_t sbytes = 0;
char buffer[1024] = {0};
strcpy(buffer, "Hello, server");
sbytes = send(sfd, buffer, strlen(buffer)+1, 0);
if(sbytes == -1){
perror("[ERROR] Fail to send.");
exit(EXIT_FAILURE);
}
close(sfd);
return 0;
}
基于 socket 接收数据需要调⽤ recv 函数, 具体信息如下。
函数头⽂件:
#include
#include
函数原型:ssize_t recv(int sockfd,void *buf,size_t len,int flags)
函数功能:基于套接字接收数据
函数参数:
sockfd : 套接字⽂件描述符
buf : 接收缓冲区的地址
len : 接收数据最⼤⻓度
flags : 标志位
函数返回值:
成功 : 返回 成功接收的字节数
失败 : 返回 -1, 并设置 errno
示例:服务器发送数据,客户端接收数据(实现客户端的接收功能)
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, const char *argv[])
{
if(argc != 3)
{
fprintf(stderr, "Usage : %s .\n" , argv[0]);
exit(EXIT_FAILURE);
}
//1.创建套接字
int sfd = socket(AF_INET, SOCK_STREAM, 0);
if(sfd == -1){
perror("[ERROR] Fail to socket.");
exit(EXIT_FAILURE);
}
struct sockaddr_in svr_addr;
bzero(&svr_addr, sizeof(struct sockaddr_in)); //清零
svr_addr.sin_family = AF_INET;
svr_addr.sin_port = htons(atoi(argv[2]));
svr_addr.sin_addr.s_addr = inet_addr(argv[1]);
int ret = connect(sfd, (const struct sockaddr *)&svr_addr, sizeof(struct sockaddr_in));
if(ret == -1){
perror("[ERROR] Fail to connect.");
exit(EXIT_FAILURE);
}
//2.获取输入并发送
ssize_t sbytes = 0;
char buffer[1024] = {0};
strcpy(buffer, "Hello, server");
sbytes = send(sfd, buffer, strlen(buffer)+1, 0);
if(sbytes == -1){
perror("[ERROR] Fail to send.");
exit(EXIT_FAILURE);
}
bzero(buffer, sizeof(buffer));
//3.接收数据并显示
ssize_t rbytes = 0;
char buffer_recv[1024] = {0};
rbytes = recv(sfd, buffer_recv, sizeof(buffer_recv), 0);
if(ret == -1){
perror("[ERROR] Fail to ercv.");
exit(EXIT_FAILURE);
}else if(rbytes > 0){
printf("buffer : %s\n", buffer_recv);
}else if(rbytes == 0){
printf("server has been shut down.\n");
}
close(sfd);
return 0;
}
将服务端的环节拆分,可得到以下流程图:
函数头⽂件:
#include
#include
函数原型:int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen)
函数功能:绑定 ip 地址与端⼝号
函数参数:
sockfd : 套接字⽂件描述符
buf : 接收缓冲区的地址
len : 接收数据最⼤⻓度
flags : 标志位
函数返回值:
成功 : 返回 成功接收的字节数
失败 : 返回 -1, 并设置 errno
示例: 设计⼀个基本的服务器程序, 完成 socket 创建并绑定 ip 地址与端⼝号
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define BACKLOG 10
int main(int argc, const char *argv[])
{
if(argc != 3)
{
fprintf(stderr, "Usage : %s .\n" , argv[0]);
exit(EXIT_FAILURE);
}
//1.创建套接字
int sfd = socket(AF_INET, SOCK_STREAM, 0);
if(sfd == -1){
perror("[ERROR] Fail to socket.");
exit(EXIT_FAILURE);
}
struct sockaddr_in svr_addr;
bzero(&svr_addr, sizeof(struct sockaddr_in)); //清零
svr_addr.sin_family = AF_INET;
svr_addr.sin_port = htons(atoi(argv[2]));
svr_addr.sin_addr.s_addr = inet_addr(argv[1]);
//2.绑定ip地址与端口号
int ret = bind(sfd, (const struct sockaddr *)&svr_addr, sizeof(struct sockaddr));
if(ret == -1){
perror("[ERROR] Failed to bind.");
exit(EXIT_FAILURE);
}
//3.设置套接字状态为监听状态,并建立监听队列
ret = listen(sfd, BACKLOG);
if(ret == -1){
perror("[ERROR] Failed to listen.");
exit(EXIT_FAILURE);
}
//4.同意建立连接——accept
struct sockaddr_in cli_addr; //用来保存客户端的地址信息
socklen_t len = sizeof(struct sockaddr_in); //用来保存地址结构体的长度
int cfd = accept(sfd, (struct sockaddr *)&cli_addr, &len);
if(cfd == -1){
perror("[ERROR] Failed to accept.");
exit(EXIT_FAILURE);
}
printf("ip : %s, port : %d\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port));
//while(1){}
close(sfd);
return 0;
}
在服务器绑定 ip 地址与端⼝号之后, 则需要让服务器 socket 套接字设置成被动监听状态,并 创建监听队列,这⾥需要调⽤ listen 函数。
函数头⽂件:
#include
#include
函数原型:int listen(int sockfd,int backlog)
函数功能:设置套接字状态为被动监听,并创建监听队列
函数参数:
sockfd : 套接字⽂件描述符
backlog : 监听队列的⻓度
函数返回值:
成功 : 返回 0
失败 : 返回 -1, 并设置 errno
函数头⽂件:
#include
#include
函数原型:int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen)
函数功能:接受来⾃于其他 socket 的连接请求,并建⽴连接
函数参数:
sockfd : 套接字⽂件描述符
addr : ⽹络地址结构的指针(输出参数,⽤于保存发送请求端的地址信息)
addrlen : ⽹络地址结构⻓度的指针 (输出参数,但是需要进⾏初始化)
函数返回值:
成功 : 返回新的⽂件描述符
失败 : -1 , 并设置 errno
示例:设计⼀个服务器程序,并和客户端建⽴连接,并打印客户端的 ip 地址和端⼝号
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define BACKLOG 10
int main(int argc, const char *argv[])
{
ssize_t rbytes, sbytes = 0; //接收send和recv返回值
char buffer[1024] = {0}; //数据缓冲区
if(argc != 3)
{
fprintf(stderr, "Usage : %s .\n" , argv[0]);
exit(EXIT_FAILURE);
}
//1.创建套接字
int sfd = socket(AF_INET, SOCK_STREAM, 0);
if(sfd == -1){
perror("[ERROR] Fail to socket.");
exit(EXIT_FAILURE);
}
struct sockaddr_in svr_addr;
bzero(&svr_addr, sizeof(struct sockaddr_in)); //清零
svr_addr.sin_family = AF_INET;
svr_addr.sin_port = htons(atoi(argv[2]));
svr_addr.sin_addr.s_addr = inet_addr(argv[1]);
//2.绑定ip地址与端口号
int ret = bind(sfd, (const struct sockaddr *)&svr_addr, sizeof(struct sockaddr));
if(ret == -1){
perror("[ERROR] Failed to bind.");
exit(EXIT_FAILURE);
}
//3.设置套接字状态为监听状态,并建立监听队列
ret = listen(sfd, BACKLOG);
if(ret == -1){
perror("[ERROR] Failed to listen.");
exit(EXIT_FAILURE);
}
//4.同意建立连接——accept
struct sockaddr_in cli_addr; //用来保存客户端的地址信息
bzero(&cli_addr, sizeof(struct sockaddr)); //清空
socklen_t len = sizeof(struct sockaddr_in); //用来保存地址结构体的长度
int cfd = accept(sfd, (struct sockaddr *)&cli_addr, &len);
if(cfd == -1){
perror("[ERROR] Failed to accept.");
exit(EXIT_FAILURE);
}
printf("ip : %s, port : %d\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port));
close(cfd);
close(sfd);
return 0;
}
添加功能:服务端打印出客户端的网络信息之后,进入死循环持续接收来自客户端发送来的消息,并将接收的消息以相同的内容发送回应给客户端。
//服务端
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define BACKLOG 10
int main(int argc, const char *argv[])
{
ssize_t rbytes, sbytes = 0; //接收send和recv返回值
char buffer[1024] = {0}; //数据缓冲区
if(argc != 3)
{
fprintf(stderr, "Usage : %s .\n" , argv[0]);
exit(EXIT_FAILURE);
}
//1.创建套接字
int sfd = socket(AF_INET, SOCK_STREAM, 0);
if(sfd == -1){
perror("[ERROR] Fail to socket.");
exit(EXIT_FAILURE);
}
struct sockaddr_in svr_addr;
bzero(&svr_addr, sizeof(struct sockaddr_in)); //清零
svr_addr.sin_family = AF_INET;
svr_addr.sin_port = htons(atoi(argv[2]));
svr_addr.sin_addr.s_addr = inet_addr(argv[1]);
//2.绑定ip地址与端口号
int ret = bind(sfd, (const struct sockaddr *)&svr_addr, sizeof(struct sockaddr));
if(ret == -1){
perror("[ERROR] Failed to bind.");
exit(EXIT_FAILURE);
}
//3.设置套接字状态为监听状态,并建立监听队列
ret = listen(sfd, BACKLOG);
if(ret == -1){
perror("[ERROR] Failed to listen.");
exit(EXIT_FAILURE);
}
//4.同意建立连接——accept
struct sockaddr_in cli_addr; //用来保存客户端的地址信息
socklen_t len = sizeof(struct sockaddr_in); //用来保存地址结构体的长度
int cfd = accept(sfd, (struct sockaddr *)&cli_addr, &len);
if(cfd == -1){
perror("[ERROR] Failed to accept.");
exit(EXIT_FAILURE);
}
printf("ip : %s, port : %d\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port));
while(1)
{
rbytes = recv(cfd, buffer, sizeof(buffer), 0);
if(rbytes == -1){
perror("[ERROR] Failed to recv.");
exit(EXIT_FAILURE);
}
else if(rbytes > 0)
{
sbytes = send(cfd, buffer, sizeof(buffer), 0);
if(rbytes == -1)
{
perror("[ERROR] Failed to recv.");
exit(EXIT_FAILURE);
}
}
else if(rbytes == 0)
{
printf("the client has been shut down.\n");
exit(EXIT_SUCCESS);
}
}
close(cfd);
close(sfd);
return 0;
}
//客户端
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, const char *argv[])
{
if(argc != 3)
{
fprintf(stderr, "Usage : %s .\n" , argv[0]);
exit(EXIT_FAILURE);
}
//1.创建套接字
int sfd = socket(AF_INET, SOCK_STREAM, 0);
if(sfd == -1){
perror("[ERROR] Fail to socket.");
exit(EXIT_FAILURE);
}
struct sockaddr_in svr_addr;
bzero(&svr_addr, sizeof(struct sockaddr_in)); //清零
svr_addr.sin_family = AF_INET;
svr_addr.sin_port = htons(atoi(argv[2]));
svr_addr.sin_addr.s_addr = inet_addr(argv[1]);
int ret = connect(sfd, (const struct sockaddr *)&svr_addr, sizeof(struct sockaddr_in));
if(ret == -1){
perror("[ERROR] Fail to connect.");
exit(EXIT_FAILURE);
}
//2.获取输入并发送
ssize_t sbytes = 0;
char buffer[1024] = {0};
strcpy(buffer, "Hello"); //这里以 Hello 作为输入
sbytes = send(sfd, buffer, strlen(buffer)+1, 0);
if(sbytes == -1){
perror("[ERROR] Fail to send.");
exit(EXIT_FAILURE);
}
bzero(buffer, sizeof(buffer));
//3.接收数据并显示
ssize_t rbytes = 0;
char buffer_recv[1024] = {0};
rbytes = recv(sfd, buffer_recv, sizeof(buffer_recv), 0);
if(ret == -1)
{
perror("[ERROR] Fail to ercv.");
exit(EXIT_FAILURE);
}
else if(rbytes > 0)
{
printf("buffer : %s\n", buffer_recv);
}
else if(rbytes == 0)
{
printf("server has been shut down.\n");
}
close(sfd);
return 0;
}
TCP是面向字节流的协议,流就像河流中的水,TCP对数据包是以“组”的方式进行发送,而并非是一次发送全部的数据包,这么做是为了提升传输效率。在这个过程中,TCP默认使用Nagle算法,而Nagle算法主要做两件事:
1)只有上一个分组得到确认,才会发送下一个分组;
2)收集多个小分组,在一个确认到来时一起发送。所以,正是Nagle算法造成了发送方有可能造成粘包现象,也就是说数据发送出来它已经是粘包的状态了。
由于分组的存在,包与包之间没有明确的界限,所以会在发送时产生粘包现象。当网络传输数据的速度大于接收方处理数据的速度时,这时候就会导致,接收方在读取缓冲区时,缓冲区存在多个数据包。而且在 TCP 协议中,接收方是一次读取缓冲区中的所有内容,所以不能反映原本的数据信息。
而UDP是基于数据报的协议,它有消息边界,所以不会出现粘包现象。
TCP粘包的解决方案主要有两种:
第二种方法是最常用的。
以第二种方法为思路,编写客户端与服务端程序。
//客户端
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc,char *argv[])
{
int sfd,ret;
struct sockaddr_in svr_addr;
ssize_t sbytes = 0;
char buffer[] = "Hello,server";
char *pbuffer = NULL;
int length = 0;
if (argc != 3)
{
fprintf(stderr,"Usage : %s < ip > < port >.\n",argv[0]) ;
exit(EXIT_FAILURE);
}
sfd = socket(AF_INET,SOCK_STREAM,0);
if (sfd == -1)
{
perror("[ERROR] Failed to socket.");
exit(EXIT_FAILURE);
}
bzero(&svr_addr,sizeof(struct sockaddr_in));
svr_addr.sin_family = AF_INET;
svr_addr.sin_port = htons(atoi(argv[2]));
svr_addr.sin_addr.s_addr = inet_addr(argv[1]);
ret = connect(sfd,(const struct sockaddr *)&svr_addr,sizeof(struct sockaddr_in));
if (ret == -1){
perror("[ERROR] Failed to connect.");
exit(EXIT_FAILURE);
}
for(;;){
length = strlen(buffer);
pbuffer = (char *)malloc(length + 4);
memcpy(pbuffer, &length,4); //给前4个字节写入数据长度
memcpy(pbuffer + 4,buffer,length); //写入数据
sbytes = send(sfd,pbuffer,length + 4,0);
if (sbytes == -1){
perror("[ERROR] Failed to send.");
exit(EXIT_FAILURE);
}
}
close(sfd);
return 0;
}
//服务端
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define BACKLOG 10
int main(int argc,char *argv[])
{
int sfd,ret,cfd;
struct sockaddr_in svr_addr,cli_addr;
ssize_t rbytes = 0,sbytes = 0;
char buffer[1024] = {0};
int length;
int total_received;
socklen_t len = sizeof(struct sockaddr_in);
if (argc != 3){
fprintf(stderr,"Usage : %s < ip > < port >.\n",argv[0]) ;
exit(EXIT_FAILURE);
}
//1.创建套接字
sfd = socket(AF_INET,SOCK_STREAM,0);
if (sfd == -1){
perror("[ERROR] Failed to socket.");
exit(EXIT_FAILURE);
}
bzero(&svr_addr,sizeof(struct sockaddr_in));
svr_addr.sin_family = AF_INET;
svr_addr.sin_port = htons(atoi(argv[2]));
svr_addr.sin_addr.s_addr = inet_addr(argv[1]);
//2.绑定ip地址与端口号
ret = bind(sfd,(const struct sockaddr *)&svr_addr,sizeof(struct sockaddr));
if (ret == -1){
perror("[ERROR] Failed to bind.");
exit(EXIT_FAILURE);
}
ret = listen(sfd,BACKLOG);
if (ret == -1){
perror("[ERROR] Failed to listen.");
exit(EXIT_FAILURE);
}
cfd = accept(sfd,(struct sockaddr *)&cli_addr,&len);
if (cfd == -1){
perror("[ERROR] Failed to accept.");
exit(EXIT_FAILURE);
}
printf("ip : %s port : %d\n",inet_ntoa(cli_addr.sin_addr),ntohs(cli_addr.sin_port));
for(;;)
{
length = 0; //存储数据长度
total_received = 0; //存储总长
rbytes = recv(cfd,&length,4,0); //读4个字节放到length里,length的值现在是数据长度
if (rbytes == -1)
{
perror("[ERROR] Failed to recv.");
exit(EXIT_FAILURE);
}
for(;;)
{
rbytes = recv(cfd, buffer + total_received, length - total_received, 0);
if (rbytes == -1)
{
perror("[ERROR] Failed to recv.");
exit(EXIT_FAILURE);
}
else if (rbytes > 0)
{
printf("buffer : %s\n",buffer);
total_received += rbytes;
if (total_received == length)
break;
}
else if (rbytes == 0)
{
printf("The client has been shutdown.\n");
break;
}
}
printf("buffer : %s\n",buffer);
sleep(1);
}
close(sfd);
return 0;
}