UDP协议为应用层提供不可靠、无连接和基于数据报的服务。所以,使用UDP协议的应用程序
通常要自己处理数据确认、超时重传等逻辑。
而TCP协议则完全相反,为应用层提供可靠的、面向连接的和基于流的服务。
字节流服务和数据报服务两者的区别对应在实际编程中,则体现为通信双方是否必须执行相同次数的读写操作。
当然了,这也只是表现形式。
当发送端应用程序连续执行多次写操作时,TCP模块先将这些数据放入TCP发送缓冲区中。当TCP模块真正发送数据时,
发送缓冲区中等待发送的数据就可能被封装成一个或者多个TCP报文段发出。因此,TCP模块发送出的TCP报文段的个数和应用程序执行的写操作次数之间没有固定的数量关系。(如下图第一行左侧图,执行三次写操作send,却只发送出两个TCP报文段)
当接收端收到一个或者多个TCP报文段后,TCP模块将他们携带的应用程序数据按照TCP报文段的序号依次放入TCP接收缓冲区中,并通知应用程序读取数据。接收端应用程序可以一次性将TCP接收缓冲区的数据全部读出,也可以分多次读取,这取决于用户指定的应用程序读缓冲区的大小。因此,应用程序执行的读操作次数和TCP模块接收到的TCP报文段个数之间也没有固定的数量关系。(如上图第一行右侧图,执行一次读操作recv,就读出两个TCP报文段)。
综上所述,发送端执行的写操作次数和接收端执行的读操作次数之间没有任何数量关系。
这就是字节流的概念:应用程序对数据的发送和接收是没有边界限制的。
UDP数据报服务则不然(上图第二行所示),发送端应用程序每执行一次写操作,UDP模块就将其封装成一个
UDP数据报并发送出去。接收端必须及时针对每一个UDP数据报执行读操作,否则就会丢包。并且,如果用户没有指定足够的应用程序缓冲区来读取UDP数据,则UDP数据将被截断。
用一个实例来说明:
下面的客户/服务器程序实现文件上传功能。说明:客户上传/服务器接收一个文件之后就关闭应用程序。
客户端程序:命令行参数指定要上传的文件名、服务器IP地址和端口号
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <unistd.h>
#define BUF_SIZE 1024
int main(int argc, char *argv[])
{
if(argc < 4)
{
printf("Usage: %s filename server_ip_address server_port\n",argv[0]);
printf("Example: %s /root/linux/server/test.txt 192.168.1.11 12345\n",argv[0]);
return 1;
}
FILE *fp; // 文件指针
//此处的fopen使用相对路径或者绝对路径都可以,并且路径中无论是否含有中文,均可解析
if((fp = fopen(argv[1],"r")) == NULL)
{
perror("fail to read.\n");
exit(1); // exit() in <stdlib.h>:exit(0)表示正常退出,exit(x)(x不为0)都表示异常退出,这个x是返回给操作系统的,以供其他程序使用。
}
const char *ip = argv[2];
int port = atoi(argv[3]);
struct sockaddr_in server_addr;
bzero(&server_addr,sizeof (server_addr));
server_addr.sin_family = AF_INET;
inet_pton(AF_INET, ip, &server_addr.sin_addr);
server_addr.sin_port = htons((unsigned short)port);
int sock = socket(PF_INET, SOCK_STREAM, 0);
assert(sock >= 0);
if(connect(sock, (struct sockaddr *)&server_addr, sizeof (server_addr)) == -1)
{
printf("connect fail!\n");
exit(1);
}
ssize_t send_ret; //记录成功发送到的字节数
//在此处可以考虑指定一个flag值,指明本次数据发送的方式.如:MSG_OOB
if((send_ret = send(sock, argv[1], strlen(argv[1]), 0)) <= 0) //首先发送本地文件的文件名给服务器端
{
printf("send filename error!\n");
exit(1);
}
else
{
printf("successfully send local filename(%zu bytes).\n", send_ret);
}
printf("正在上传文件......\n");
//sleep(3); //此处阻塞,让客户先发送一个关于文件名的TCP报文段,同时也可等待服务器程序从其接收缓冲区中读取文件名数据
char read_buf[BUF_SIZE]; //读取本地文件内容的缓冲区
unsigned long len; //记录读取的每行字符的个数
while (fgets(read_buf, BUF_SIZE, fp))
{
len = strlen(read_buf);
/* read_buf[len-1] = '\0'; //去掉每行末尾换行符,这样我们自己可以控制读到的数据的输出格式 */
/* printf("%s\n",read_buf); // unsigned long ---> %lu */
/* 此处,通过TCP连接将文件读取缓冲区的内容发送给服务器端,所以无须客户端自己控制其格式化输出 */
ssize_t ret;
if((ret=send(sock, read_buf, len, 0)) <= 0)
{
printf("send data error!\n");
exit(1);
}
else
{
printf("send %zu bytes data\n",ret);
}
}
close(sock);
fclose(fp);
return 0;
}
服务器程序:命令行参数指定本机IP地址和端口号
#include <stdio.h>
#include <libgen.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
#define BUF_SIZE 1024 //接收缓冲区大小
int main(int argc, char *argv[])
{
if(argc < 3)
{
printf("Usage: %s ip_address port_number\n",basename(argv[0]));
printf("Example: %s 192.168.1.11 12345\n",basename(argv[0]));
exit(1);
}
const char *ip = argv[1];
int port = atoi(argv[2]);
struct sockaddr_in addr;
bzero(&addr, sizeof (addr));
addr.sin_family = AF_INET;
inet_pton(AF_INET, ip, &addr.sin_addr);
addr.sin_port = htons((unsigned short)port);
int sockfd = socket(PF_INET, SOCK_STREAM, 0);
assert(sockfd >= 0);
int ret = bind(sockfd, (struct sockaddr *)&addr, sizeof (addr));
assert(ret != -1);
ret = listen(sockfd,10);
assert(ret != -1);
struct sockaddr_in client;
int client_addrlen = sizeof (client);
int connfd = accept(sockfd, (struct sockaddr *)&client, (socklen_t *)&client_addrlen);
if(connfd < 0)
{
printf("connect fail!\n");
exit(1);
}
else
{
printf("Receive some data from client computer!\n");
char client_addr[INET_ADDRSTRLEN];
printf("Client's ip address is %s\n",inet_ntop(AF_INET, &client.sin_addr, client_addr, INET_ADDRSTRLEN));
}
char filename_buf[BUF_SIZE];
memset(filename_buf, '\0',BUF_SIZE);
ssize_t recv_ret; //记录成功读取到的字节数
if((recv_ret=recv(connfd, filename_buf, BUF_SIZE-1, 0)) < 0)
{
printf("read filename failed!\n");
}
else
{
printf("successfully receive filename(%zu bytes).\n",recv_ret);
}
FILE *fp = fopen(filename_buf,"w");
if(fp == NULL)
{
printf("create file fail.\n");
exit(1);
}
else
{
printf("create file successfully.\n");
}
char recv_buf[BUF_SIZE]; // 可以调用memset/bzero
memset(recv_buf, '\0', BUF_SIZE);
while ((recv(connfd, recv_buf, BUF_SIZE, 0)) > 0)
{
fprintf(fp, "%s",recv_buf);
//每次接收完数据并且读到文件中之后,都要清空接收缓冲区!!!!!!!!!!!!!!!
//这一点非常重要,否则该接收缓冲区中将存在上一次接收的数据,所以造成数据重复与错误.
memset(recv_buf, '\0', BUF_SIZE);
}
fclose(fp);
close(connfd);
close(sockfd);
return 0;
}
运行结果如下
服务端:
客户端:
结果分析:在传输文件的过程中,先传输的是文件名。但是,可以看到客户端发送了8字节的文件名,但是服务端接收的文件名却是37字节。这是为什么呢? 仔细观察客户端上传的数据,文件名字节数与之后的四次发送数据的字节数相加刚好等于37字节。这就说明了,客户端执行了5次写操作,服务端就执行了1次读操作。而服务端由于读取到的多余的数据不能作为文件名的合法名字,所以服务端创建文件失败并退出程序,客户端也由于服务器断开连接,剩余的数据发送失败。
以上,就体现出了字节流的概念:应用程序对数据的发送和接收是没有边界限制的
我们可以稍微修改客户端程序,就能让此客户/服务器程序完成预期功能。
可以看到,我们在客户端程序中,在完成文件名的发送之后,对一个系统调用sleep()进行了注释,现在将其注释删掉让该sleep调用阻塞客户端程序3秒钟,这样就可以在文件名写操作之后有足够的时间让内核TCP模块封装文件名数据发送给服务端,服务端也有时间正确读取文件名来创建文件。正确结果如下:
服务端:
客户端: