前言
在上一篇博文中,我们实现了回声服务器端/客户端程序,并且还遗留一个回声客户端程序存在的一个问题。在本篇博文中,我们将给出遗留问题的解决方案,并详细讨论TCP的工作原理。
一 回声客户端的完美实现
1.1 回声服务器端没有问题,只有回声客户端有问题?
问题不在服务器端,而在客户端。但只看代码也许不太好理解,因为I/O中使用了相同的函数。先回顾一下回声服务器端的I/O习惯代码,下面是 echo_server.c 的I/O操作相关代码:
while((str_len=read(clnt_sock, message, BUF_SIZE)) !=0 )
write(clnt_sock, message, str_len);
接着回顾回声客户端代码,下面是 echo_client.c 的I/O操作相关代码:
write(sock, message, strlen(message));
str_len=read(sock, message, BUF_SIZE-1);
二者都是循环调用read或write函数。实际上之前的回声客户端将100%接收自己传输的数据,只不过接收数据时的单位有些问题。扩展客户端代码,下面是 echo_client.c 收发数据的I/O操作代码:
while(1)
{
fputs("Input message(Q to quit): ", stdout); //标准输出
fgets(message, BUF_SIZE, stdin); //标准输入
if(!strcmp(message,"q\n") || !strcmp(message,"Q\n")) //如果输入字符q或Q,则退出循环体
break;
write(sock, message, strlen(message)); //向服务器端发送字符串消息
str_len=read(sock, message, BUF_SIZE-1); //接收来自服务器端的消息
message[str_len]= '\0'; //在字符数组尾部添加字符串结束符'\0'
printf("Message from server: %s", message); //输出接收到的消息字符串
}
从上面的代码可以看出,回声服务器传输的是字符串,而且是通过调用write函数一次性发送的。之后还调用一次read函数,期待着接收自己传输的字符串。这就是问题所在!
“既然回声客户端会收到所有字符串数据,是否只需多等一会儿?过一段时间后再调用read函数是否可以一次性读取所有字符串数据呢?”
的确,过一段时间后即可接收,但需要等多久?要等10分钟吗?这不符合常理,理想的客户端应在收到字符串数据时立即读取并输出。
这里我们有必要对read()函数的返回值情况做一下说明:
//read 函数原型
ssize_t read(int fd, void *buf, size_t count);
1、如果读取成功,则返回实际读到的字节数。这里又有两种情况:一是如果在读完count要求字节之前已经到达文件的末尾,那么实际返回的字节数将小于count值,但是仍然大于0;二是在读完count要求字节之前,仍然没有到达文件的末尾,这时实际返回的字节数等于要求的count值。
2、如果读取时已经到达文件的末尾,则返回0。
3、如果出错,则返回1。
这样也就是说分为>0、<0、=0三种情况进行讨论。在有的时候,<0、=0可以合为一种情况进行处理。这要根据程序的实际情况进行处理。
1.2 回声客户端问题解决方法
解决的办法是使用循环,每次调用read函数就立即返回实际接收到的数据大小。如果实际接收到的数据量小于期望收到的,则循环调用read函数,直至接收到所有的数据。因此,我们可以修改一下 echo_client.c 代码,修改后的代码如下:
- echo_client2.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
void error_handling(char *message);
int main(int argc, char *argv[])
{
int sock;
char message[BUF_SIZE];
int str_len, recv_len, recv_cnt; //新增两个整型变量recv_len,recv_cnt
struct sockaddr_in serv_adr;
if(argc!=3) {
printf("Usage: %s <IP> <port>\n", argv[0]);
exit(1);
}
sock=socket(PF_INET, SOCK_STREAM, 0); //创建客户端TCP套接字
if(sock==-1)
error_handling("socket() error");
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
serv_adr.sin_port=htons(atoi(argv[2]));
if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1) //调用connect函数,向服务器端发起连接请求
error_handling("connect() error!");
else
puts("Connected...........");
while(1)
{
fputs("Input message(Q to quit): ", stdout); //标准输出
fgets(message, BUF_SIZE, stdin); //标准输入
if(!strcmp(message,"q\n") || !strcmp(message,"Q\n")) //如果输入字符q或Q,则退出循环体
break;
str_len = write(sock, message, strlen(message)); //向服务器端发送字符串消息,并返回发送的数据量大小
//@override
recv_len = 0; //初始化接收数据量大小变量
while(recv_len < str_len)
{
recv_cnt = read(sock, &message[recv_len], BUF_SIZE-1); //接收来自服务器端的消息,并返回实际接收到的数据量
if(recv_cnt == -1) //如果接收出错,打印错误信息
error_handling("read() error!");
recv_len += recv_cnt; //累加接收到的数据量
}
message[str_len]= '\0'; //在字符数组尾部添加字符串结束符'\0'
printf("Message from server: %s", message); //输出接收到的消息字符串
}
close(sock); //关闭客户端套接字
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
在 echo_client.c 代码中只调用了一次read函数,而在echo_client2.c 代码中为了接收所有传输数据而循环调用了read函数,while循环条件是 while(recv_len < str_len),也可以写成:while(recv_len != str_len) 的形式,但是更推荐前面使用前一种循环条件。因为假设发生异常情况,读取数据过程中 recv_len 超过了 str_len,此时就无法退出循环,有可能引发无限循环。而如果while循环条件是第一种形式,则即使发生异常也不会陷入无限循环。写循环语句时应尽量降低因异常情况而陷入无限循环的可能。
1.3 如果问题不在于回声客户端:定义应用层协议
回声客户端可以提前知道接收的数据长度,但我们需要意识到的是,在更多情况下是无法预先知道要接收的数据长度的。既然如此,若无法预知接收数据长度时应如何收发数据呢?此时需要的就是应用层协议的定义。之前的回声服务器端/客户端程序中定义了如下协议:
“收到Q或q就立即终止TCP连接。”
同样,收发数据过程中也需要定好规则(协议)以表示数据的边界,或提前告知收发数据的大小。服务器端/客户端实现过程中逐步定义的这些规则集合就是应用层协议。可以看出,应用层协议并不是高深莫测,只不过是为特定程序的实现而制定的规则。
1.4 实现计算器服务器端/客户端程序
下面编写一个示例程序以体验应用层协议的定义过程。该程序中,服务器端从客户端获得多个数字和运算符信息。服务器端收到数字后对其进行加减乘运算,然后把计算结果传回客户端。例如,向服务器端传递3、5、9的同时请求加法运算,则客户端收到 3+5+9 的运算结果;若请求做乘法运算,则客户端收到 3×5×9 的运算结果。而如果向服务器传递4、3、2 的同时要求做减法,则客户端将收到 4-3-2 的运算结果,即第一个参数成为被减少。
在编写程序之前,我们需要先设计一下应用层协议。为了简单起见,我们只设计了最低标准的协议,在实际的应用程序实现中需要的协议更详细、更准确。应用层协议规则定义如下:
- 客户端连接到服务器端后以1字节整数形式传递待运算数字个数。
- 客户端向服务器端传递的每个整数型数据占用4字节。
- 传递整数型数据后接着传递运算符。运算符信息占用1字节。
- 选择字符 +、-、* 之一传递。
- 服务器端以4字节整数型向客户端传回运算结果。
- 客户端得到运算结果后终止与服务器端的连接。
这种程度的协议相当于实现了一半程序,这也说明应用层协议设计在网络编程中的重要性。只要设计好协议,实现程序就不会成为大问题。另外要记住的一点,调用close()函数将向通信对端传递 EOF,请各位记住这一点并加以运用。
我们先实现计算器客户端程序代码。
- 计算器客户端程序 op_client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
#define OPSZ 4 //操作数占用字节数
#define RLT_SIZE 4 //运算结果数占用字节数
void error_handling(char *message);
int main(int argc, char *argv[])
{
int sock;
char opmsg[BUF_SIZE] = {0};
int result, opnd_cnt, i;
struct sockaddr_in serv_addr;
if(argc != 3){
printf("Usage: %s <IP> <port>\n", argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_STREAM, 0);
if(sock == -1)
error_handling("socket() error!");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
error_handling("connect() error!")
else
puts("Connected...........");
fputs("Operand count: ", stdout);
scanf("%d", &opnd_cnt); //输入操作数个数
opmsg[0] = (char)opnd_cnt; //将操作符个数存入字符数组,占用1个字节
for(i=0; i<opnd_cnt; i++)
{
printf("Operand %d: ", i+1);
scanf("%d", (int*)&opmsg[i*OPSZ+1]); //将4字节整型数保存到字符数组中,需要将其转换成int指针类型
}
fgetc(stdin); //标准输入一个字符
fputs("Operator: ", stdout); //标准输出
scanf("%c", &opmsg[opnd_cnt*OPSZ+1]); //将操作符存入字符数组,占用1个字节
write(sock, opmsg, opnd_cnt*OPSZ+2); //发送数据给服务器端
read(sock, &result, RLT_SIZE); //接收运算结果数据,存入result变量中
printf("Operation result: %ld\n", result);
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
我们给出客户端向服务器端传送的数据的数据格式示例,如下图所示:
从图1-1 中可以看出,若想在同一数组中保存并传输多种数据结构,应把数组声明为char类型。而且需要额外做一些指针及数组运算。
接下来我们实现服务器端的程序代码,如下所示:
- 计算器服务器端程序 op_server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
#define OPSZ 4 //操作数占用字节数
void error_handling(char *message);
int calculate(int opnum, int opnds[], char operator);
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
char opinfo[BUF_SIZE] = {0};
int result, opnd_cnt;
int recv_cnt, recv_len;
struct sockaddr_in serv_addr, clnt_addr;
socklen_t clnt_addr_sz;
if(argc != 2){
printf("Usage: %s <port>\n", argv[0]);
exit(1);
}
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if(serv_sock == -1)
error_handling("socket() error!");
memset(serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
error_handling("bind() error!");
if(listen(serv_sock, 5) == -1)
error_handling("listen() error!")
clnt_addr_sz = sizeof(clnt_addr);
for(i=0; i<5; i++)
{
opnd_cnt = 0;
clnt_sock = accept(serv_addr, (struct sockaddr*)&clnt_addr, &clnt_addr_sz);
read(clnt_sock, &opnd_cnt, 1); //读取1字节操作数个数,存入opnd_cnt变量中
recv_len = 0;
while(opnd_cnt*OPSZ+1 > recv_len) //循环读取剩余的数据
{
recv_cnt = read(clnt_sock, &opinfo[recv_len], BUF_SIZE);
recv_len += recv_cnt;
}
result = calculate(opnd_cnt, (int*)opinfo, opinfo[recv_len-1]);
write(clnt_sock, (char*)&result, sizeof(result)); //向客户端传回运算结果消息
close(clnt_sock);
}
close(serv_sock);
return 0;
}
int calculate(int opnum, int opnds[], char op)
{
int result = opnds[0], i;
swith(op)
{
case '+':
for(i=1; i<opnum; i++) result += opnds[i];
break;
case '-':
for(i=1; i<opnum; i++) result -= opnds[i];
break;
case '*':
for(i=1; i<opnum; i++) result *= opnds[i];
break;
}
return result;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
- 运行结果
- 服务器端
编译程序:gcc op_server.c -o opserver
运行程序:./opserver 9190
- 客户端
编译程序:gcc op_client.c -o opclient
运行结果1:./opclient 127.0.0.1 9190
Connected...........
Operand count: 3
Operand 1: 12
Operand 2: 24
Operand 3: 36
Operator: +
Operation result: 72
运行结果2:./opclient 127.0.0.1 9190
Connected...........
Operand count: 2
Operand 1: 24
Operand 2: 12
Operator: -
Operation result: 12
《结果分析》从运行结果可以看出,客户端首先询问用户待输入数字的个数,再输入相应个数的整数,最后以运算符的形式输入符号信息,并输出运算结果(+、-、* 之一)。当然,实际的运算操作是由服务器端做的,客户端只是接收运算结果并输出给用户。
二 TCP原理
2.1 TCP套接字中的I/O缓冲
我们已经知道,TCP套接字的数据收发无边界。服务器端即使调用1次write函数传输40字节的数据,客户端也有可能通过调用4次read函数每次读取10字节。但此处也有一些疑问,服务器端一次性传输了40字节,而客户端居然可以缓慢地分批接收。客户端接收10字节后,剩下的30字节在何处等候呢?是不是像飞机为了等待着陆而在空中盘旋一样,剩下的30字节也在网络中徘徊并等等接收呢?
实际上,write函数调用后并非立即传输数据,read函数调用后也并非马上接收数据。更准确地说,如下图所示,write函数调用瞬间,数据被移至输出缓冲区(即发送缓冲区);read函数调用瞬间,从输入缓冲区(即接收缓冲区)读取数据。
如上图2-2所示,调用write函数时,数据被移至输出缓冲,在适当的时候(不管是分别发送还是一次性发送)传向对端的输入缓冲。这是对方将调用read函数从输入缓冲读取数据。这些I/O缓冲特性可整理如下。
- I/O缓冲在每个TCP套接字中单独存在。
- I/O缓冲在创建套接字时自动生成。
- 即使关闭套接字也会继续传递输出缓冲中遗留的数据。
- 关闭套接字将丢失输入缓冲(即接收缓冲)中的数据。
那么下面这种情况会引发什么事情?
“客户端输入缓冲(即接收缓冲)为50字节,而服务器传输了100字节。”
这的确是一个问题。接收缓冲只有50字节,却收到了100字节的数据。可以提出如下解决方案:
“填满接收缓冲前迅速调用read函数读取数据,这样会腾出一部分空闲的缓冲空间,问题就解决了”
但是TCP协议有流量控制机制,因此 “不会发生超过接收缓冲大小的数据传输”。
也就是说,根本不会发生这类问题,TCP协议利用滑动窗口(Sliding Window)机制来实现流量控制。
所谓流量控制(flow control)就是让发送发的发送速率不要太快,要让接收方来得及接收。用对话方式呈现如下:
- 套接字A:“你好,最多可以向我传递50字节数据。”
- 套接字B:“OK!”
- 套接字A:“我腾出了20字节的空间,最多可以接收70字节数据。”
- 套接字B:“OK!”
数据收发也是如此,因此TCP中不会因为缓冲溢出而丢失数据。
《提示》从write函数返回的时间点
write函数并不是在向通信对端传输完所有数据时才返回,而是在数据被移到TCP套接字的发送缓冲时就返回了。但TCP会保证对发送缓冲数据的传输,所以说write函数在数据传输完成时返回,我们要准确理解这句话的真正内涵。
2.2 TCP内部工作原理1:与对方套接字的连接
TCP套接字从创建到消失所经历过程分为如下3部。
- 与对方套接字建立连接。
- 与对方套接字进行数据交换。
- 断开与对方套接字的连接。
我们首先讲解与对方套接字建立连接的过程。连接过程中套接字之间的对话如下:
- [Shake 1] 套接字A:“你好,套接字B。我这儿有数据要传给你,建立连接吧。”
- [Shake 2] 套接字B:“好的,我这边已就绪。”
- [Shake 3] 套接字C:“谢谢你受理我的连接请求。”
TCP在实际连接建立过程中也会经过3次对话过程。因此,该过程又称 “Three-way handshaking(三报文握手)”。接下来给出连接过程中实际交换的信息格式,如下图所示:
TCP套接字是以全双工(Full-duplex)方式工作的。也就是说,它可以双向传递数据,即可接收,也可发送。因此,正式收发数据前需要做一些准备工作。
1、首先,请求连接的主机A向主机B传递如下信息:
[SYN] SEQ: 1000, ACK: -
该消息中 SEQ为1000,ACK为空,而SEQ为1000的含义是:“现传递的数据报的初始序号为1000,如果接收无误,请通知我向您传递1001号数据包。”
这是首次请求连接时使用的消息,又称SYN(同步)。SYN 是 Synchronization 的简写,表示收发数据前传输的同步消息。
2、接下来主机B向主机A传递如下消息:
[SYN+ACK] SEQ: 2000, ACK: 1001
此时SEQ为2000,ACK为1001,而SEQ为2000的含义是:“现传递的数据包初始序号为2000,如果接收无误,请通知我向您传递2001号数据包。”
而ACK: 1001 的含义是:“刚才传输的SEQ为1000的数据包接收无误,现在请传递SEQ为1001的数据包。”
对主机A首次传输的数据包的确认消息(ACK:1001)和为主机B传输数据做准备的同步消息(SEQ:2000)捆绑发送,因此,此种类型的消息又称为 SYN+ACK。
通信双方收发数据前向数据包分配初始序号,并向对方通知此序号,这都是为了防止数据丢失所做的准备。通过向数据包分配序号并确认,可以在数据丢失时马上查看并重传丢失的数据包。因此,TCP可以保证可靠的数据传输。
3、最后主机A向主机B传递如下消息:
[ACK] SEQ: 1001, ACK: 2001
因为主机A发送的 SYN 数据包需要消耗一个序号,因此此刻主机A发送的第二个数据包的序号在之前的序号1000的基础上加1,也就是分配1001。此时该数据包传递的信息含义是:“已正确收到传输的SEQ为2000的数据包,现在可以传输SEQ为2001的数据包了。”
至此,主机A和主机B的TCP连接就建立成功了,接下来就可以进行数据传递操作了。
《TCP连接建立过程说明》
1、TCP传送数据的传输单位是TCP报文段,上面所说的数据包就是指的TCP报文段。
2、TCP报文段 = 首部 + 数据部分
3、TCP对TCP报文段分配序号,是以1个字节数据为单位分配的,而不是以TCP报文段个数为单位分配的。
4、在TCP三报文握手建立连接的过程中,SYN报文段需要消耗一个序号,即使该报文段的数据部分为空。
5、TCP报文段首部中的序号字段是该报文段的数据部分第1个数据字节的序号值。相关内容详情可以去了解TCP报文段的首部格式。
2.2 TCP内部工作原理2:与对方主机的数据交换
通过第一步三报文握手过程成功建立起了TCP连接,完成了数据交换的准备工作,就下来就可以正式开始收发数据过程。
1、其默认方式如下图所示:
上图2-4 给出了主机A分2次(分2个TCP报文段)向主机B传递200字节数据的过程。首先,主机A通过第一个报文段发送100个字节的数据,报文段的SEQ为1200。主机B为了确认收到该报文段,向主机B发送 ACK 1301 确认。
此时的ACK号(确认号)为1301而非1201,原因在于ACK号的增量为传输的数据字节数。假设每次ACK号不加传输的字节数,这样虽然可以确认报文段的传输,但无法明确100字节数据是全部正确传递还是丢失了一部分,比如只传递了80字节。因此按如下公式传递ACK消息:
ACK号 = SEQ号 + 传递的数据字节数 + 1
与三报文握手过程相同,最后加1是为了告知对方下次要传递的SEQ号。
2、传输数据过程中报文段丢失的情况,如下图所示:
上图2-5 表示通过SEQ 1301 报文段向主机B传递100字节的数据。但中间发生了错误,主机B并未收到。经过一段时间后,主机A仍未收到对于 SEQ 1301 的ACK确认,因此主机A会重传该报文段。为了完成报文段的重传,TCP套接字会启动超时计时器以等待ACK应答。若超时计时器发生超时(Time-out)则重传。
2.3 TCP内部工作原理3:断开与套接字的连接
TCP套接字断开连接的过程也非常优雅。如果对方还有数据需要传输时直接断开连接会出现问题,所以断开连接时需要双方进行友好协商。断开连接时双方对话如下:
- 套接字A:“我希望断开连接。”
- 套接字B:“哦,是吗?请稍后。”
- 套接字B:“我也准备就绪,可以断开连接。”
- 套接字A:“好的,谢谢合作。”
先由套接字A向套接字B传递断开连接的消息,套接字B发出确认收到的消息,然后向套接字A传递可以断开连接的消息,套接字A同样发出确认消息,如下图所示:
上图2-6中,报文段内的 FIN 表示断开连接。也就是说,双方各发送1次 FIN 报文段后断开连接。此过程经历4个阶段,因此又称四报文握手(Four-way handshaking)。SEQ 和 ACK 的含义与前面讲解的含义一样。在上图2-6中,主机B向主机A传递了两次 ACK 5001,这是因为第二次FIN 报文段中的ACK 5001 只是因为接收ACK消息后未接收数据而重传给主机A的,以便其在要发出的第四个确认报文段中知晓自己的SEQ。
三 习题
1、请说明TCP套接字连接建立的三次握手过程。尤其是3次数据交换过程每次收发的数据内容。
第一次握手:客户端发送 SYN同步报文段,设置初始序号seq=x,并进入到 SYN-SENT状态,等待服务器端的确认。
第二次握手:服务器端收到客户端的SYN报文段后,回复 SYN+ACK 报文段,设置初始序号seq=y,确认号ack=x+1,并进入到 SYN-RCVD状态。
第三次握手:客户端收到服务器端发来的 SYN+ACK报文段后,回复 ACK 确认报文段,序号seq=x+1,确认号ack=y+1,并进入到 ESTABLISHED 状态,当服务器端收到 ACK报文段后,也将进入到 ESTABLISHED 状态。至此,TCP连接建立成功。
2、TCP是可靠的数据传输协议,但在通过网络通信的过程可能丢失数据。请通过ACK和SEQ说明TCP通过何种机制保证丢失数据的可靠传输。
答:TCP通过在TCP报文段首部中设置SEQ(序号)和ACK(确认号)字段,就可以知道传输的数据是否正确地被通信对端接收。SEQ表示当前发送的TCP报文段的第一个数据字节的序号,ACK表示期望收到对方下一个报文段的第一个数据字节的序号。当收到某个确认报文段时,若确认号ACK=N,则表明到序号 N-1 为止的所有数据对方都已正确收到。若等待确认报文段超时,则说明传输的数据可能丢失,需要重传。
3、TCP 套接字中调用 write 和 read 函数时数据如何移动?结合 I/O 缓冲进行说明。
答:一个TCP套接字是有独立地接收缓冲和发送缓存的,它们是操作系统内核区分配的内存空间。当TCP套接字调用write函数时,就是将待发送数据移至TCP的发送缓冲区中,而调用read函数时,就是将接收到的数据移至TCP的接收缓冲区。
4、对方主机的接收缓冲剩余 50 字节空间时,若本主机通过 write 函数请求传输 70 字节,请问 TCP 如何处理这种情况?
答:根据TCP的流量控制机制,接收端主机会向发送端通知自己的接收窗口大小,作为让发送端设置其发送窗口大小的依据。因此,发送端的发送窗口只能先设置为50字节,然后发送出去50字节的数据,接收端收到这50字节的数据后,此时接收端的接收缓冲已满,接收端返回确认报文段,并通知发送端自己的接收窗口为零了,于是发送端就停止发送数据。当接收端的接收缓冲区中的数据被读取走后,并有足够的缓冲空间来接收剩余的数据时,接收端会通知发送方可以继续发送数据了,于是发送端就将剩余的20字节数据发送给接收端。
5、更改之前博文中编写的tcp_server.c 和 tcp_client.c程序,使服务器端和客户端各传递1次字符串。考虑到使用TCP传输协议,所以传递字符串前先以4字节整数型方式传递字符串长度。连接时服务器端和客户端数据传输格式如下。
另外,不限制字符串传输顺序及种类,但须进行3次数据交换。
相关博文链接:Linux网络编程 - 套接字与协议族 — 3.5 面向连接的套接字:TCP套接字示例
- 服务器端程序 sendrecv_serv.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
void error_handling(char *message);
int main(int argc, char *argv[])
{
int serv_sock;
int clnt_sock;
int str_len, i;
struct sockaddr_in serv_addr;
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_sz;
char msg1[]="Hello client!";
char msg2[]="I'm server.";
char msg3[]="Nice to meet you.";
char *str_arr[]={msg1, msg2, msg3}; //指针数组指向三个字符串
char read_buf[100] = {0};
if(argc!=2){
printf("Usage: %s <port>\n", argv[0]);
exit(1);
}
serv_sock=socket(PF_INET, SOCK_STREAM, 0);
if(serv_sock == -1)
error_handling("socket() error");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family=AF_INET;
serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_addr.sin_port=htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1)
error_handling("bind() error");
if(listen(serv_sock, 5)==-1)
error_handling("listen() error");
clnt_addr_sz=sizeof(clnt_addr);
clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_addr,&clnt_addr_sz);
if(clnt_sock==-1)
error_handling("accept() error");
for(i=0; i<3; i++) //进行3次数据交互
{
str_len=strlen(str_arr[i])+1; //字符串长度+1是包含了字符串结束符'\0'
write(clnt_sock, (char*)(&str_len), 4); //发送字符串长度的整型数信息
write(clnt_sock, str_arr[i], str_len); //发送字符串信息
read(clnt_sock, (char*)(&str_len), 4); //读取字符串长度整型数信息
read(clnt_sock, read_buf, str_len); //读取字符串信息,并存入字符数组中
puts(read_buf);
}
close(clnt_sock);
close(serv_sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
- 客户端程序 recvsend_clnt.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
void error_handling(char *message);
int main(int argc, char* argv[])
{
int sock;
struct sockaddr_in serv_addr;
char msg1[]="Hello server!";
char msg2[]="I'm client.";
char msg3[]="Nice to meet you too!";
char* str_arr[]={msg1, msg2, msg3};
char read_buf[100];
int str_len, i;
if(argc!=3){
printf("Usage: %s <IP> <port>\n", argv[0]);
exit(1);
}
sock=socket(PF_INET, SOCK_STREAM, 0);
if(sock == -1)
error_handling("socket() error");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family=AF_INET;
serv_addr.sin_addr.s_addr=inet_addr(argv[1]);
serv_addr.sin_port=htons(atoi(argv[2]));
if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1)
error_handling("connect() error!");
for(i=0; i<3; i++) //进行3次数据交互
{
read(sock, (char*)(&str_len), 4); //读取字符串长度整型数信息
read(sock, read_buf, str_len); //读取字符串信息,并存入字符数组中
puts(read_buf);
str_len=strlen(str_arr[i])+1; //字符串长度+1是包含了字符串结束符'\0'
write(sock, (char*)(&str_len), 4); //发送字符串长度的整型数信息
write(sock, str_arr[i], str_len); //发送字符串信息
}
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
6、实现收发文件的服务器/客户端程序,实现顺序如下。
- 客户端接收用户输入的传输文件名。
- 客户端请求服务器传输该文件名所指的文件。
- 如果该文件存在,服务器端就将其发送给客户端;反之,则回复文件不存在的提示信息。
- 断开连接。
- 文件服务器端程序 file_serv.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
#define FILE_SIZE 30
void error_handling(char *message);
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
char buf[BUF_SIZE] = {0};
char file_name[FILE_SIZE] = {0};
FILE *fp;
int read_cnt;
struct sockaddr_in serv_addr, clnt_addr;
socklen_t clnt_addr_sz;
if(argc!=2){
printf("Usage: %s <port>\n", argv[0]);
exit(1);
}
serv_sock=socket(PF_INET, SOCK_STREAM, 0);
if(serv_sock == -1)
error_handling("socket() error");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family=AF_INET;
serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_addr.sin_port=htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1)
error_handling("bind() error");
if(listen(serv_sock, 5)==-1)
error_handling("listen() error");
clnt_addr_sz=sizeof(clnt_addr);
while(1)
{
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_sz);
if(clnt_sock==-1)
error_handling("accept() error");
read(clnt_sock, file_name, FILE_SIZE); //接收文件名信息
fp = fopen(file_name, "rb"); //以二进制只读方式打开文件
if(!fp){
sprintf(buf, "%s file does`t exist!", file_name);
write(clnt_sock, buf, BUF_SIZE); //发送文件不存在提示信息
}
else{
while(1) //循环体中发送文件内容
{
read_cnt = fread(buf, 1, BUF_SIZE, fp); //读取文件内容,按字节单位读取BUF_SIZE个元素
if(read_cnt < BUF_SIZE) //文件内容读取完毕的条件
{
write(clnt_sock, buf, read_cnt);
break;
}
write(clnt_sock, buf, BUF_SIZE);
}
}
fclose(fp);
close(clnt_sock);
memset(buf, 0, BUF_SIZE);
}
close(serv_sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
- 文件客户端程序 file_clnt.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
#define FILE_SIZE 30
void error_handling(char *message);
int main(int argc, char *argv[])
{
int sock;
FILE *fp;
char buf[BUF_SIZE] = {0};
char file_name[FILE_SIZE] = {0};
int read_cnt;
struct sockaddr_in serv_addr;
if(argc!=3) {
printf("Usage: %s <IP> <port>\n", argv[0]);
exit(1);
}
printf("Input file name: ");
scanf("%s", file_name);
fp=fopen(file_name, "wb"); //以二进制可写方式打开文件
if(!fp)
error_handling("fopen() error!");
sock=socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family=AF_INET;
serv_addr.sin_addr.s_addr=inet_addr(argv[1]);
serv_addr.sin_port=htons(atoi(argv[2]));
if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
error_handling("connect() error!")
write(sock, file_name, strlen(file_name)+1); //发送文件名字符串,+1表示包含了字符串结束符'\0'
while((read_cnt=read(sock, buf, BUF_SIZE))!=0) //在while循环中将收到的read_cnt大小的文件数据块按字节单位写入文件
fwrite((void*)buf, 1, read_cnt, fp);
fclose(fp);
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
参考
《TCP-IP网络编程(尹圣雨)》第5章 - 基于TCP的服务器端/客户端(2)