TCP/IP网络编程C05-基于TCP的服务端⁄客户端(2)
学习笔记
TCP原理
TCP报文头部格式(至少20字节)
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Port(16bits) | Destination Port(16bits) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Sequence Number(32bits) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Acknowledgement Number(32bits) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data | |U|A|P|R|S|F| |
| Offset| Reserved |R|C|S|S|Y|I| Window(16bits) |
|(4bits)| (6bits) |G|K|H|T|N|N| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Checksum(16bits) | Urgent Pointer(16bits) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Options | Padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Sequence Number: 此TCP报文段的第一个八位字节数据的序列号(存在SYN时除外),若存在SYN,则序列号为初始序列号(ISN),且第一个八位字节数据为ISN+1
Acknowledgement Number: 如果设置了ACK控制位,则此字段包含TCP报文的发送方希望接收的下一个序列号的值(发送方发送的序列号+1)。一旦建立了连接,就会发送该连接。
控制位:
URG: 紧急
ACK: 确认
PSH: 传送
RST: 重置
SYN: 连接
FIN: 结束
Data Offset: TCP报文首部的长度
Reserved: 留至后用
Window: 控制数据量
Checksum: 传输数据完整性校验
Urgent Pointer: 将紧急数据插入到报文数据的最前面

TCP连接的建立(三报文握手)
1.客户端发送一个连接请求报文段,报文首部中的SYN控制位置1,并设置一个序列号(Sequence Number)seq = x
2.服务端返回一个确认报文段,报文首部中的SYN与ACK控制位都置1,并设置一个自己的序列号seq = y,一个确认号(Acknowledgement Number)ack = x + 1
3.客户端再发送一个确认报文段,报文首部ACK控制位置1,设置确认号ack = y + 1,序列号seq = z(若未携带数据,seq = x + 1)

TCP连接的结束(四报文挥手)
1.客户端发送一个连接释放报文段,报文首部中的FIN控制位置1,并设置一个序列号seq = u
2.服务端返回一个确认报文段,报文首部中的SYN与ACK控制位置1,并设置一个自己的序列号seq = v,确认号ack = u + 1,TCP连接进入半关闭状态(服务端仍可发送数据)
3.若服务端已再不用发送数据,则服务端发送一个连接释放报文段,报文首部FIN控制位置1,设置序号seq = w,ack = u + 1
4.客户端再发送一个确认报文段,报文首部ACK控制位置1,设置确认号ack = w + 1

TCP套接字中的I/O缓冲
write()函数调用后并非立即传输数据,在write()调用瞬间,数据移至输出缓冲;read()函数调用后也并非马上接收数据,在read()调用瞬间,其从输入缓冲读取数据

I/O缓冲特性:
- I/O缓冲在每个TCP套接字中单独存在
- I/O缓冲在创建套接字时自动生成
- 即使关闭套接字也会继续传递输出缓冲中遗留的数据
- 关闭套接字将丢失输入缓冲中的数据
TCP协议通过滑动窗口协议,不会因缓冲溢出而丢失数据
程序实现方法
为了实现以上功能,定义了一个简单的应用层协议,用来约定在服务器端和客户端之间传输数据的规则。
协议内容包括:
-
客户端用 1 个字节整数形式传递操作数的个数。
-
客户端向服务器端传送的每个操作数占用 4 字节。
-
传递完操作数后紧跟着传递一个占用 1 字节的运算符。
-
服务器端以 4 字节整数向客户端传回运算结果。
-
客户端得到运算结果后终止与服务器端的连接。
习题答案
Q01
-
请说明 TCP 套接字连接设置的三次握手过程。尤其是 3 次数据交换过程每次收发的数据内容。

TCP套接字的生命周期主要可分为3个部分: ①与对方套接字建立连接 ②与对方套接字进行数据交换 ③断开与对方套接字的连接。
其中,在第一步建立连接的阶段,又可细分为3个步骤(即三次握手):①由主机1给主机2发送初始的SEQ,首次连接请求是关键字是SYN,表示收发数据前同步传输的消息,此时报文的ACK一般为空。②主机2收到报文以后,给主机 1 传递信息,用一个新的SEQ表示自己的序号,然后ACK代表已经接受到主机1的消息,希望接受下一个消息③主机1收到主机2的确认以后,还需要给主机2给出确认,此时再发送一次SEQ和ACK。
Q02
-
TCP 是可靠的数据传输协议,但在通过网络通信的过程可能丢失数据。请通过 ACK 和 SEQ 说明 TCP 通过何种机制保证丢失数据的可靠传输。
SEQ 顺序标识符是给信息编号。ACK 是用于回复带有编号的信息。也就是说,每次传输信息时,都同时发送 SEQ 标识,接收它的主机需要根据SEQ信息向发送它的主机发送ACK消息。通过这种机制,传输数据的主机就可以确认数据是否被正确接收。在传输失败时,可以重新传送。
Q03
-
TCP 套接字中调用 write 和 read 函数时数据如何移动?结合 I/O 缓冲进行说明
当调用write函数时,数据被移动到套接字的输出缓冲中。然后,这些移动的数据将基于TCP协议发送到对方主机套接字的输入缓冲,这样存储在输入缓冲中的数据将通过read函数的调用读取。
Q04
-
对方主机的输入缓冲剩余 50 字节空间时,若本方主机通过 write 函数请求传输 70 字节,问 TCP 如何处理这种情况?
对方主机会把输入缓冲中可存储的数据大小传送给要传输数据的数据(本方)。因此,在空闲空间为50byte的情况下,即使请求发送70byte的数据,也不会进行超过50byte的传输。剩下的在存储在传输方的输出缓冲中,等待对方主机的输入缓冲有空闲。而且,这种交换缓冲区可用空间信息的协议称为滑动窗口,这也是作为TCP一部分存在的协议。
Q05
5.第2章示例 tcp_server.c(第1章的 hello_server.c)和 tcp_client.c中,客户端接收服务器端传输的字符串后便退出。现更改程序,使服务器端和客户端各传递1次字符串。考虑到使用TCP协议,所以传递字符串前先以4字节整数型方式传递字符串长度。连接时服务器端和客户端数据传输格式如下。

另外,不限制字符串传输顺序及种类,但须进行3次数据交换。
/**********************************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_size;
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];
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_size=sizeof(clnt_addr);
clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_addr,&clnt_addr_size);
if(clnt_sock==-1)
error_handling("accept() error");
for(i=0; i<3; i++)
{
str_len=strlen(str_arr[i])+1;
write(clnt_sock, (char*)(&str_len), 4);
write(clnt_sock, str_arr[i], str_len);
read(clnt_sock, (char*)(&str_len), 4);//取到字符串的总长度str_len
read(clnt_sock, read_buf, str_len);//读取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++)
{
read(sock, (char*)(&str_len), 4);//取到字符串的总长度str_len
read(sock, read_buf, str_len);//读取str_len长度的字符串
puts(read_buf);
str_len=strlen(str_arr[i])+1;
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);
}
Q06
- 创建收发文件的服务器端/客户端,实现顺序如下。
- 客户端接受用户输入的传输文件名。
- 客户端请求服务器端传输该文件名所指文件
- 如果指定文件存在,服务器端就将其发送给客户端;反之,则断开连接。
/**********************************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 30
void error_handling(char *message);
int main(int argc, char *argv[])
{
int serv_sd, clnt_sd;
FILE * fp;
char buf[BUF_SIZE];
char file_name[BUF_SIZE];
int read_cnt;
struct sockaddr_in serv_adr, clnt_adr;
socklen_t clnt_adr_sz;
if(argc!=2) {
printf("Usage: %s <port>\n", argv[0]);
exit(1);
}
serv_sd=socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_adr.sin_port=htons(atoi(argv[1]));
bind(serv_sd, (struct sockaddr*)&serv_adr, sizeof(serv_adr));
listen(serv_sd, 5);
clnt_adr_sz=sizeof(clnt_adr);
clnt_sd=accept(serv_sd, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);
read(clnt_sd, file_name, BUF_SIZE);
fp=fopen(file_name, "rb");
if(fp!=NULL)
{
while(1)
{
read_cnt=fread((void*)buf, 1, BUF_SIZE, fp);
//size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)
if(read_cnt<BUF_SIZE)
{
write(clnt_sd, buf, read_cnt);
break;
}
write(clnt_sd, buf, BUF_SIZE);
}
}
fclose(fp);
close(clnt_sd);
close(serv_sd);
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/socket.h>
#define BUF_SIZE 30
void error_handling(char *message);
int main(int argc, char *argv[])
{
int sd;
FILE *fp;
char buf[BUF_SIZE];
char file_name[BUF_SIZE];
int read_cnt;
struct sockaddr_in serv_adr;
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");
sd=socket(PF_INET, SOCK_STREAM, 0);
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]));
connect(sd, (struct sockaddr*)&serv_adr, sizeof(serv_adr));
write(sd, file_name, strlen(file_name)+1);
while((read_cnt=read(sd, buf, BUF_SIZE))!=0)
fwrite((void*)buf, 1, read_cnt, fp);
fclose(fp);
close(sd);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
书本源码
01-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;
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);
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)
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"))
break;
str_len=write(sock, message, strlen(message));
recv_len=0;
while(recv_len<str_len)//循环调用read()函数(事先已经知道要从服务器获取的总byte数)
{
recv_cnt=read(sock, &message[recv_len], BUF_SIZE-1);
if(recv_cnt==-1)
error_handling("read() error!");
recv_len+=recv_cnt;
}
message[recv_len]=0;
printf("Message from server: %s", message);
}
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
/******************** input******************
*description:
*content:
*******************************************/
/******************** output******************
*description:
*content:
*******************************************/
02-echo_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
void error_handling(char *message);
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
char message[BUF_SIZE];
int str_len, i;
struct sockaddr_in serv_adr;
struct sockaddr_in clnt_adr;
socklen_t clnt_adr_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_adr, 0, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_adr.sin_port=htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
error_handling("bind() error");
if(listen(serv_sock, 5)==-1)
error_handling("listen() error");
clnt_adr_sz=sizeof(clnt_adr);
for(i=0; i<5; i++)
{
clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);
if(clnt_sock==-1)
error_handling("accept() error");
else
printf("Connected client %d \n", i+1);
while((str_len=read(clnt_sock, message, BUF_SIZE))!=0)
write(clnt_sock, message, str_len);
close(clnt_sock);
}
close(serv_sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
03-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 oprator);
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
char opinfo[BUF_SIZE];
int result, opnd_cnt, i;
int recv_cnt, recv_len;
struct sockaddr_in serv_adr, clnt_adr;
socklen_t clnt_adr_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_adr, 0, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_adr.sin_port=htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
error_handling("bind() error");
if(listen(serv_sock, 5)==-1)
error_handling("listen() error");
clnt_adr_sz=sizeof(clnt_adr);
for(i=0; i<5; i++)
{
opnd_cnt=0;
clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);
read(clnt_sock, &opnd_cnt, 1);//首先读取总操作数的数量(1个字节)
recv_len=0;//记录接下来接收到的总字节数
while((opnd_cnt*OPSZ+1)>recv_len)
{
recv_cnt=read(clnt_sock, &opinfo[recv_len], BUF_SIZE-1);
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;
switch(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);
}
/******************** input******************
*description:
协议内容包括:
客户端用 1 个字节整数形式传递操作数的个数。
客户端向服务器端传送的每个操作数占用 4 字节。
传递完操作数后紧跟着传递一个占用 1 字节的运算符。
服务器端以 4 字节整数向客户端传回运算结果。
客户端得到运算结果后终止与服务器端的连接。
*content:
./03-op_server 9190
*******************************************/
/******************** output******************
*description:
*content:
*******************************************/
04-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 RLT_SIZE 4
#define OPSZ 4
void error_handling(char *message);
int main(int argc, char *argv[])
{
int sock;
char opmsg[BUF_SIZE];
int result, opnd_cnt, i;
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);
if(sock==-1)
error_handling("socket() error");
//填写目的端的IP和端口
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)
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]);//第2-5个字节存储一个int数字,第6-9个字节存储下一个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);//接收计算后的结果
printf("Operation result: %d \n", result);
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
/******************** input******************
*description:
协议内容包括:
客户端用 1 个字节整数形式传递操作数的个数。
客户端向服务器端传送的每个操作数占用 4 字节。
传递完操作数后紧跟着传递一个占用 1 字节的运算符。
服务器端以 4 字节整数向客户端传回运算结果。
客户端得到运算结果后终止与服务器端的连接。
*content:
./04-op_client 127.0.0.1 9190
*******************************************/
/******************** output******************
*description:
*content:
onnected...........
Operand count: 3
Operand 1: 11
Operand 2: 12
Operand 3: 13
Operator: +
Operation result: 36
*******************************************/

浙公网安备 33010602011771号