赞
踩
TCP套接字是面向连接的,因此又称基于流(stream)的套接字。
TCP,Transmission Control Protocol,传输控制协议
四层网络模型
四层TCP/IP协议栈
链路层:最底层,定义LAN 、WAN 、MAN等网络标准
数据链路层通过各种控制协议,将有差错的物理信道变为无差错的、能可靠传输数据帧的数据链路,为IP 层提供数据传送服务。
IP层:传输路径选择。IP本身是面向消息的、不可靠的协议。lP协议无法应对数据错误。
网络层通过路由选择算法,为分组选择最适当的路径,实现两个端系统之间的数据透明传送。
TCP/UDP:以IP层提供的路径信息完成实际的数据传输。IP只是负责传输数据包,不关心顺序、丢失或损坏的情况,TCP解决这些问题
应用层:在进行网络编程时,前3层的实现都被隐藏在socket之中,不需要我们多加操作。利用套接字编写程序,根据程序特点决定服务器端和客户端之间的数据传输规则(规定),这便是应用层协议。网络编程的大部分内容就是设计并实现应用层协议。
以前面的 hello_server.c 服务器端和 hello_client.c 客户端为例,说明这个流程(注意顺序)
(1)服务器端:调用 socket() 和 bind() 创建套接字并为套接字分配地址信息
(2)服务器端:调用 listen() 进入等待连接请求状态
只有服务器端调用了listen函数,客户端才能进入可发出连接请求的状态(调用connect())。若客户端提前调用connect()将报错
listen()并不代表建立连接,它只是创建了一个连接请求队列,这个队列由第2个参数backlog决定大小。当客户端发起连接请求时,listen将其加入到连接请求队列之中,等待着连接受理。
(3)客户端:调用 connect() 发出连接请求(默认已用socket()创建套接字)
调用connect()之后,只有服务器端“接收连接请求”或者“发生断网等异常情况而中断连接请求”后才能返回值
这里的“接收连接”并不是指服务器端调用accept(),而是指请求被listen()记录到了等待队列之中
因此connect()返回后,并不能立即进行数据交换,因为此时连接尚未受理
(4)服务器端:调用 accept() 受理客户端连接请求
int accept(int sock, struct sockaddr *addr, socklen_t *addrlen)
sock选择监听套接字;addr保存客户端地址信息。
accept()受理sock创建的等待队列中的连接请求。它将自动创建一个新的套接字,并自动与该客户端建立连接,数据传输。
如果此时等待队列中为空,accept()将不会返回(阻塞),直到有连接请求到来为止
为什么要新建套接字:sock本身是一个监听套接字,如果将它用于和客户端的通信,那么它就无法正常接收新的连接请求了(难以区分哪些是请求连接的套接字信息,哪些是传输的数据),因此对于每一个客户端的连接请求,都要新建一个对应的套接字。
(5)服务器端/客户端:调用 read() write() 传输数据
accept()后,就有了一对一用于传输数据的套接字。此时,服务器端和客户端就可以向指定的socket进行读写了
(6)服务器端/客户端:close() 关闭套接字
思考:为什么客户端不用bind()为套接字分配地址信息?
客户端使用socket()之后立即使用connect(),似乎并未替socket分配地址信息。但实际上,网络数据交换的双方必须都分配IP地址和端口号。客户端的IP地址和端口号在调用connect()时由操作系统自动分配,它的IP地址即是自身计算机IP地址,端口号随机分配。
connect()的第1个参数是自己的sockfd,这个sockfd会被自动绑定客户端的IP地址和端口号;第2个参数就是服务器端的IP地址和端口号
(客户端连接服务器端时,需要服务器端的端口号。服务器端回复客户端时,也需要客户端的端口号。这两个端口号不一致)
服务器端和客户端不断互传信息,直到客户端输入Q为止
注意:此时服务器端在同一时刻只能处理一个客户端 —— 依次处理5个客户端的请求
#include <stdio.h> #include <stdlib.h> #include <arpa/inet.h> #include <sys/socket.h> #include <string.h> #include <unistd.h> #define BUF_SIZE 1024 void error_handling(const char* message){ fputs(message, stderr); fputc('\n', stderr); exit(1); } int main(int argc, char* argv[]){ int serv_sock, clnt_sock; struct sockaddr_in serv_addr, clnt_addr; int clnt_addr_size; int i, str_len; char message[BUF_SIZE]; //1. 从这里开始和之前的 hello_server.c 一样 if(argc != 2) { error_handling("wrong argc"); exit(1); } //socket serv_sock = socket(PF_INET, SOCK_STREAM, 0); if(serv_sock == -1) error_handling("socket() error!"); //bind 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!"); //listen if(listen(serv_sock, 5) == -1) error_handling("listen() error!"); //2. 使用循环,依次接受accept clnt_addr_size = sizeof(clnt_addr); for(i=0; i<5; ++i){ clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size); if(clnt_sock == -1) error_handling("accept() error!"); else printf("Connected client %d \n", i+1); //反复接收数据,再返回给客户端;每次读取BUF_SIZE个字节 while((str_len=read(clnt_sock, message, BUF_SIZE)) != 0) write(clnt_sock, message, str_len); close(clnt_sock); } close(clnt_sock); return 0; }
#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) { fputs(message, stderr); fputc('\n', stderr); exit(1); } int main(int argc, char* argv[]) { int sock; struct sockaddr_in serv_addr; char message[BUF_SIZE]; int str_len; //1. 前面的部分和hello_client.c差不多 if(argc != 3) { error_handling("wrong argc"); exit(1); } //socket sock = socket(PF_INET, SOCK_STREAM, 0); if(sock == -1) error_handling("socket() error!"); //connect 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 ..."); //若调用该函数引起的连接请求被注册到服务器端等待队列,则connect函数将完成正常调用。 //即使输出了“connected ...”,但如果服务器尚未调用accept函数,也不会真正建立服务关系。 //2. 反复发送数据,再反复接收 while(1) { fputs("Input message(Q to quit): ", stdout); fgets(message, BUF_SIZE, stdin); //strcmp相等返回0 if(!strcmp(message, "q\n") || !strcmp(message, "Q\n")) break; write(sock, message, strlen(message)); str_len = read(sock, message, BUF_SIZE-1); message[str_len] = 0; printf("Message from server: %s", message); } close(sock);//调用close()向服务器发送EOF(意味着中断连接) return 0; }
echo服务器端/客户端存在的问题:
场景:服务器端从socket中读取数据,每次读多少就发送给客户端多少。设想当数据量很大的情况,此时服务器端需要多次write才能发送完毕。客户端有可能在服务器端write完毕之前就调用了read(),导致读取不完整
解决:下一章详细说明
思考:关于accep()
首先,服务器端创建 serv_sock,这个serv_sock绑定了服务器端的ip地址和设置的端口号
然后,客户端创建 sock,这个sock绑定了自己的地址和端口,通过connect发送到服务器端
服务器端 accept 接收到请求,解析客户端 sock 中的地址和端口,并创建一个新的 clnt_sock
服务器端创建的serv_sock和clnt_sock,文件描述符fd分别是3,4
客户端创建的sock,文件描述符为3
服务器端和客户端创建的3个socket是不一样的
服务器端读写在clnt_sock,客户端读写在sock
问题:clnt_sock是怎么和sock传输数据的?(socket连接)
(1)修改头文件,添加 WSAStartup() 语句
(2)修改变量类型名字,如socket的返回值类型要从 int 修改为 SOCKET
(3)bind()函数的返回值判断由 -1 修改为 SOCKET_ERROR
(4)read/write 修改为 send/recv (参数变为4个);close变为closesocket
都是一些小的修改,和前面linux下的代码差不多的。
回顾前面的迭代服务器端/客户端的问题:无法达到TCP无数据边界的要求
如数据太大,服务器端需要多次write才能发送完毕。客户端有可能在服务器端write完毕之前就调用了read(),导致读取不完整
服务器端收发数据的代码:
//反复读取,每次读多少就返回给客户端多少
while((str_len=read(clnt_sock, message, BUF_SIZE)) != 0)
write(clnt_sock, message, str_len);
客户端收发数据的代码:
while(1) { fputs("Input message(Q to quit): ", stdout); fgets(message, BUF_SIZE, stdin); if(!strcmp(message, "q\n") || !strcmp(message, "Q\n")) break; //重点语句 //write写数据没有问题,每次将字符串写完 write(sock, message, strlen(message)); //read的问题:假设服务器端发送的慢,一次read不能读完整怎么办? //服务器端没有这个问题是因为,服务器端用的是while+read,而不是一次性read //服务器端不断read/write,不关心每次传输的数据大小。客户端应当确定每次read多少,每次write多少 str_len = read(sock, message, BUF_SIZE-1); message[str_len] = 0; printf("Message from server: %s", message); }
一种修改办法,是在客户端读取时,while循环+指定读取大小,如下
//控制客户端读取的字节数
str_len = write(sock, message, strlen(message));
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[recv_len] = 0;
这种方式需要客户端自己清楚要接收的大小(这种很多情况做不到)。
收发数据过程中定好规则(协议)以表示数据的边界,或提前告知收发数据的大小。服务器端/客户端实现过程中逐步定义的这些规则集合就是应用层协议。例如:“收到Q就终止连接”
在应用层协议中可以定好数据边界的表示方法、数据的长度范围等。
案例:客户端向服务器端发送加减乘除的运算要求,服务器端计算结果并返回给客户端
(之后再回来看存在哪些问题)
op_server.c
clnt_addr_size = sizeof(clnt_addr); for(i=0; i<5; ++i) { num_count = 0; clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_size); printf("client fd: %d\n", clnt_sock); while(1) { if((read_len=read(clnt_sock, buf, BUF_SIZE)) != 0)//0表示到达结尾 { if(read_len == -1) error_handling("read() error"); if(buf[0]<'0' || buf[0]>'9') break; nums[num_count++] = atoi(buf); printf("%d\n", nums[num_count-1]); } else break; } op = buf[0]; printf("op is %c\n", op); res = 0; switch(op) { case '+': for(j=0; j<num_count; ++j) res+=nums[j]; break; default: printf("no such operator!\n"); break; } printf("return result: %d\n", res); putchar('\n'); sprintf(buf, "%d", res); write(clnt_sock, buf, sizeof(buf)); close(clnt_sock); }
op_client.c
fputs("Operand count: ", stdout);
scanf("%d", &num_count);
getchar();
for(i=0; i<=num_count; ++i){
if(i<num_count)
printf("Operand %d: ", i+1);
else
printf("Operator: ");
fgets(message, BUF_SIZE, stdin);
write(sock, message, strlen(message));
}
str_len = read(sock, message, BUF_SIZE-1);
message[str_len] = 0;
printf("Message from server: %s\n", message);
定义协议如下:
(1)客户端连接到服务器端后以1字节整数形式传递待运算数字个数
(2)客户端向服务器端传递的每个整数型数据占用4字节
(3)传递整数型数据后接着传递运算符。运算符信息占用1字节
(4)选择字符+,-,*之一传递
(5)服务器端以4字节返回运算结果
(6)客户端得到运算结果之后终止与服务器端的连接
若想在数组中保存并传输多种类型,应当声明为 char 类型
!!从协议的角度解决问题时,尽可能从指针和字节的角度理解read和write,像 int 和 char 这样的类型理解为 4字节 和 1字节
协议就是规定双方的读写规则
先看客户端,它要做更多的事情
#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) { fputs(message, stderr); fputc('\n', stderr); exit(1); } int main(int argc, char* argv[]) { int sock; struct sockaddr_in serv_addr; int opnd_cnt, i; char opmsg[BUF_SIZE]; int result; if(argc != 3) { error_handling("wrong argc"); 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); //协议1:用1个字节保存运算数的个数,最高为255个,即 0b11111111,更大的会被截断 opmsg[0] = (char)opnd_cnt; for(i=0; i<opnd_cnt; ++i) { printf("Operand %d: ", i+1); //协议2: 用4个字节保存每一个运算数,OPSZ为自定义的4字节 scanf("%d", (int *)&opmsg[i*OPSZ+1]);//这行代码详解放在了下面 } //协议3:用1个字节保存运算符+、-、* fgetc(stdin);//读取字符时要注意缓冲区残留的'\n'问题 fputs("Operator: ", stdout); scanf("%c", &opmsg[opnd_cnt*OPSZ+1]); //协议4:服务器端返回的结果用4个字节保存 write(sock, opmsg, opnd_cnt*OPSZ+2); read(sock, &result, RLT_SIZE);//RLT_SIZE也是4字节,result是int类型 printf("Message from server: %d\n", result); close(sock); return 0; }
关于scanf读取int型数据,但存放在 char str[] 数组的过程:
scanf("%d", (int *)&opmsg[i*OPSZ+1]);//每个运算数不管大小要占4个字节
//相当于以int读取一个数,例如384,它占4个字节,即32bit,二进制是 (0b)00000000 00000000 00000001 10000000
//将这4个字节放到 &opmsg[i*OPSZ+1] 这一地址指向的内存空间,因为opmsg是char类型数组,一个元素占1字节,因此要占4个元素
//假设下标从1开始,从数值的角度来看,则opmsg[1]=0b10000000=-128, opmsg[2]=0b00000001=1, opmsg[3]=0, opmsg[4]=0;
//即使不用(int *)显式说明也是可以的
//服务器端再将opmsg转为int型(例如一次性读4个字节),就能得到对应的int数值
和客户端采用同样的协议规则,重点在几条协议的实现代码上
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <sys/socket.h> #define BUF_SIZE 1024 #define OPSZ 4 void error_handling(const char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); } 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; default: printf("no such operator!"); break; } return result; } int main(int argc, char *argv[]) { int serv_sock, clnt_sock; struct sockaddr_in serv_addr, clnt_addr; int i, j, clnt_addr_size; char opinfo[BUF_SIZE]; int opnd_cnt, recv_len, recv_cnt; int result; if(argc != 2) { error_handling("wrong argc"); 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); for(i=0; i<5; ++i) { opnd_cnt = 0; clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_size); printf("connect socket: fd = %d\n", clnt_sock); //协议1:用1个字节保存运算数的个数。这里将1字节的内容转给4字节的int类型 read(clnt_sock, &opnd_cnt, 1); //下面服务端将所有传输过来的内容放到opinfo里面 recv_len = 0; //opnd_cnt*OPSZ+1 表示剩下还有多少字节没有读取 while((opnd_cnt*OPSZ+1)>recv_len) { recv_cnt = read(clnt_sock, &opinfo[recv_len], BUF_SIZE-1); recv_len += recv_cnt; } //协议2: 用4个字节保存每一个运算数,OPSZ为自定义的4字节 //协议3:用1个字节保存运算符+、-、* //这里用int *强转为int arr[],arr的最后一个元素是没有意义的运算符 result=calculate(opnd_cnt, (int *)opinfo, opinfo[recv_len-1]); printf("return value = %d\n", result); //协议4:服务器端返回的结果用4个字节保存 write(clnt_sock, (char *)&result, sizeof(result)); close(clnt_sock); } close(serv_sock); return 0; }
TCP使用滑动窗口,保证每次传输的数据不会超过接收方的输入缓冲的剩余空间大小
write/send 函数返回的时间节点:在数据移动到发送方的输出缓冲后即返回,TCP会保证输出缓冲中的数据传输给接收方
套接字是以全双工( Full-duplex )方式工作的。也就是说,它可以双向传递数据
以A向B发起连接为例
(1)第1次 A --> B
[SYN] SEQ: 1000, ACK: -
SEQ sequence,表示序号;代表这是当前A传输给B的数据包,序号是1000
ACK 确认消息,表示希望从对方那里收到的下一个数据包序号。- 表示空,意味着这是首次连接
“现传递的数据包序号为1000 ,如果接收无误,请通知我向您传递1001号数据包。”
(2)第2次 B --> A
[SYN+ACK] SEQ: 2000, ACK: 1001
SEQ 代表这是B传给A的数据包,序号是2000
ACK 确认消息,表示之前已经收到过A传给B的1000,希望下次收到的是1001
“现传递的数据包序号为2000 ,如果接收无误,请通知我向您传递2001 号数据包。“
(3)第3次 A --> B
[ACK] SEQ: 1001, ACK 2001
含义与第2次类似。TCP保证了有序传输
到这里,A和B各发送了一次确认消息ACK,确认了彼此均就绪
每次收到的确认号 ACK = SEQ + 传输的字节数 + 1
加上传输的字节数,是为了检查是否所有数据都被收到了
最后加1,是为了告知对方下次希望收到的 SEQ
TCP处理传输错误的情况:
接收方每收到一个数据包,都会发送ACK确认,如果数据包丢失,就不会发送ACK。
发送方发送一个数据包后,超过一定时间还没有收到ACK,就认为数据包丢失,于是重传数据包
(即便发送方超时重传后又收到了第一次发送的ACK,也没有关系。因为接收方收到重复的数据包会舍弃,被舍弃的数据包不会为它发送ACK)
快速重传:
A发送 S1, S2, S3, S4 共4个数据包给B,S1顺利到达,于是B返回确认号 A2;
S2延迟或丢失,于是S3和S4先一步到达。此时发现还是没有S2,于是发出的确认号都是 A2。
A收到3个一样的确认号 A2,就知道S2缺失,于是发送S2
FIN表示断开连接,双方各发送一次FIN,并且各收到一次ACK后,连接断开,又称为四次握手
注意的是,中间主机B连续两次发送数据包
主机B的FIN中,再一次使用 ACK 5001 是表示上一次只是为了接收ACK,并没有接收A发来的数据,所以要求A重传一次
收发文件的服务器端和客户端
存在的问题:当服务器端和客户端之间有数据传输时,服务器端调用close(),客户端接收到的是-1,而不是0
原因:只要TCP栈的读缓冲里还有未读取(read)数据,则调用close时会直接向对端发送RST,而不是FIN
解决方法:在close()之前,使用shutdown(clnt_sock, SHUT_WR)。详见第7章
file_server.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <sys/socket.h> #include <fcntl.h> #define BUF_SIZE 1024 void error_handling(const char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); } int main(int argc, char *argv[]) { int serv_sock, clnt_sock; struct sockaddr_in serv_addr, clnt_addr; int i, clnt_addr_size, file_fd, read_len; char buf[BUF_SIZE]; char file_name[BUF_SIZE]; if(argc != 2) { error_handling("wrong argc"); 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); for(i=0; i<5; ++i) { clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_size); printf("connected client fd = %d\n", clnt_sock); if(clnt_sock == -1) error_handling("accept error"); read(clnt_sock, file_name, BUF_SIZE-1); printf("file name = %s\n", file_name); file_fd = open(file_name, O_RDONLY); if(file_fd == -1) { close(clnt_sock); fputs("can not find the file!", stdout); break; } while((read_len = read(file_fd, buf, BUF_SIZE-1)) > 0) { printf("read size: %d\n", read_len); write(clnt_sock, buf, read_len); } close(file_fd); close(clnt_sock); } close(serv_sock); return 0; }
file_client.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #include <fcntl.h> #define BUF_SIZE 1024 #define NAME 64 void error_handling(char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); } int main(int argc, char* argv[]) { int sock; struct sockaddr_in serv_addr; int recv_len, i, file_fd; char buf[BUF_SIZE]; char file_copy[BUF_SIZE]; if(argc != 3) { error_handling("wrong argc"); 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("enter the file name: ", stdout); scanf("%s", file_copy); write(sock, file_copy, sizeof(file_copy)); strcat(file_copy, "_copy.c"); while((recv_len = read(sock, buf, BUF_SIZE-1)) != 0) { if(recv_len == -1) { error_handling("read() error!"); } //注意,如果用open是新建一个文件,最好加上八进制的权限设置,如0644(权限设置和linux文件操作一样) file_fd = open(file_copy, O_CREAT|O_APPEND|O_WRONLY, 0644); write(file_fd, buf, recv_len); close(file_fd); } close(sock); return 0; }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。