下面我们实现下回声客户端。
所谓“回声”,是指客户端向服务器发送一条数据,服务器再将数据原样返回给客户端。
代码相对于 篇一 与 篇二 并没有太多变化。如下所示:
服务器端:
#include <cstdio> #include <unistd.h> #include <stdlib.h> #include <cstring> #include <cassert> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> const int BUFFER_SIZE = 4096; const int SERVER_PORT = 2222; int main() { int server_socket; char buff[BUFFER_SIZE]; int n; server_socket = socket(AF_INET, SOCK_STREAM, 0); assert(server_socket != -1); struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(SERVER_PORT); server_addr.sin_addr.s_addr = htonl(INADDR_ANY); assert(bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) != -1); assert(listen(server_socket, 5) != -1); struct sockaddr_in client_addr; socklen_t client_addr_len = sizeof(client_addr); while(1) { printf("waiting...\n"); int connfd = accept(server_socket, (struct sockaddr*)&client_addr, &client_addr_len); if(connfd == -1) continue; printf("connect from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); n = recv(connfd, buff, BUFFER_SIZE, 0); send(connfd, buff, n, 0); close(connfd); } close(server_socket); return 0; }
客户端:
#include <cstdio> #include <unistd.h> #include <stdlib.h> #include <cstring> #include <cassert> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> const int BUFFER_SIZE = 4096; const int SERVER_PORT = 2222; int main() { int client_socket; const char *server_ip = "127.0.0.1"; char buffSend[BUFFER_SIZE]; char buffRecv[BUFFER_SIZE]; int n; fgets(buffSend, BUFFER_SIZE, stdin); client_socket = socket(AF_INET, SOCK_STREAM, 0); assert(client_socket != -1); struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(SERVER_PORT); server_addr.sin_addr.s_addr = inet_addr(server_ip); assert(connect(client_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) != -1); assert(send(client_socket, buffSend, strlen(buffSend), 0) != -1); n = recv(client_socket, buffRecv, BUFFER_SIZE, 0); buffRecv[n] = '\0'; printf("echo: %s\n", buffRecv); close(client_socket); return 0; }
Socket缓冲区
每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。
Socket的写函数并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由 TCP / UDP 协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是 TCP / UDP 协议负责的事情。
TCP / UDP 协议独立于 Socket读写 函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。
Socket 的I/O缓冲区示意图
I/O缓冲区特性如下:
1)I/O缓冲区在每个 Socket 中单独存在;
2)I/O缓冲区在创建 Socket 时自动生成;
3)即使关闭 Socket 也会继续传送输出缓冲区中遗留的数据;
4)关闭 Socket 将丢失输入缓冲区中的数据。
阻塞模式
对于TCP套接字(默认情况下),当使用 write()/send() 发送数据时:
1) 首先会检查缓冲区,如果缓冲区的可用空间长度小于要发送的数据,那么 write()/send() 会被阻塞(暂停执行),直到缓冲区中的数据被发送到目标机器,腾出足够的空间,才唤醒 write()/send() 函数继续写入数据;
2) 如果TCP协议正在向网络发送数据,那么输出缓冲区会被锁定,不允许写入,write()/send() 也会被阻塞,直到数据发送完毕缓冲区解锁,write()/send() 才会被唤醒;
3) 如果要写入的数据大于缓冲区的最大长度,那么将分批写入;
4) 直到所有数据被写入缓冲区 write()/send() 才能返回。
当使用 read()/recv() 读取数据时:
1) 首先会检查缓冲区,如果缓冲区中有数据,那么就读取,否则函数会被阻塞,直到网络上有数据到来;
2) 如果能读取的数据长度小于缓冲区中的数据长度,那么就不能一次性将缓冲区中的所有数据读出,剩余数据将不断积压,直到有 read()/recv() 函数再次读取;
3) 直到读取到数据后 read()/recv() 函数才会返回,否则就一直被阻塞;
这就是TCP套接字的阻塞模式。所谓阻塞,就是上一步动作没有完成,下一步动作将暂停,直到上一步动作完成后才能继续,以保持同步性。
以上说明:
数据的接收和发送是无关的,read()/recv() 函数不管数据发送了多少次,都会尽可能多的接收数据。也就是说,read()/recv() 和 write()/send() 的执行次数可能不同。
例如,write()/send() 重复执行三次,每次都发送字符串"abc",那么目标机器上的 read()/recv() 可能分三次接收,每次都接收"abc";也可能分两次接收,第一次接收"abcab",第二次接收"cabc";也可能一次就接收到字符串"abcabcabc"。
假设我们希望客户端每次发送一位学生的学号,让服务器端返回该学生的姓名、住址、成绩等信息,这时候可能就会出现问题,服务器端不能区分学生的学号。例如第一次发送 1,第二次发送 3,服务器可能当成 13 来处理,返回的信息显然是错误的。
这就是 TCP协议 数据的“粘包”问题,客户端发送的多个数据包被当做一个数据包接收,也称数据的无边界性,read()/recv() 函数不知道数据包的开始或结束标志(实际上也没有任何开始或结束标志),只把它们当做连续的数据流来处理。
下面的代码演示了粘包问题,客户端连续三次向服务器端发送数据,服务器端却一次性接收到所有数据:
服务器端:
#include <cstdio> #include <unistd.h> #include <stdlib.h> #include <cstring> #include <cassert> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> const int BUFFER_SIZE = 4096; const int SERVER_PORT = 2222; int main() { int server_socket; char buff[BUFFER_SIZE]; int n; server_socket = socket(AF_INET, SOCK_STREAM, 0); assert(server_socket != -1); struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(SERVER_PORT); server_addr.sin_addr.s_addr = htonl(INADDR_ANY); assert(bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) != -1); assert(listen(server_socket, 5) != -1); struct sockaddr_in client_addr; socklen_t client_addr_len = sizeof(client_addr); while(1) { printf("waiting...\n"); int connfd = accept(server_socket, (struct sockaddr*)&client_addr, &client_addr_len); if(connfd == -1) continue; printf("connect from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); sleep(5); //等待5s后再接收数据,其实就是保证客户端发送的数据都已到达服务器端输入缓冲区 n = recv(connfd, buff, BUFFER_SIZE, 0); send(connfd, buff, n, 0); close(connfd); } close(server_socket); return 0; }
客户端:
#include <cstdio> #include <unistd.h> #include <stdlib.h> #include <cstring> #include <cassert> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> const int BUFFER_SIZE = 4096; const int SERVER_PORT = 2222; int main() { int client_socket; const char *server_ip = "127.0.0.1"; char buffSend[BUFFER_SIZE]; char buffRecv[BUFFER_SIZE]; int n; fgets(buffSend, BUFFER_SIZE, stdin); client_socket = socket(AF_INET, SOCK_STREAM, 0); assert(client_socket != -1); struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(SERVER_PORT); server_addr.sin_addr.s_addr = inet_addr(server_ip); assert(connect(client_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) != -1); for(int i = 0; i < 3; ++i) //重复发送3次 assert(send(client_socket, buffSend, strlen(buffSend), 0) != -1); n = recv(client_socket, buffRecv, BUFFER_SIZE, 0); buffRecv[n] = '\0'; printf("echo: %s\n", buffRecv); close(client_socket); return 0; }
其实就是对上述代码稍作修改即可。
优雅的断开连接--shutdown()
调用 close() 函数意味着完全断开连接,即不能发送数据也不能接收数据,这种“生硬”的方式有时候会显得不太“优雅”。
close() 断开连接
主机A发送完数据后,单方面调用 close() 断开连接,之后主机A、B都不能再接受对方传输的数据。实际上,是完全无法调用与数据收发有关的函数。
一般情况下这不会有问题,但有些特殊时刻,需要只断开一条数据传输通道,而保留另一条。
使用 shutdown() 函数可以达到这个目的,它的原型为:
int shutdown(int sock, int howto);
howto 在 Linux 下有以下取值:
1)SHUT_RD:断开输入流。套接字无法接收数据(即使输入缓冲区收到数据也被抹去),无法调用输入相关函数。
2)SHUT_WR:断开输出流。套接字无法发送数据,但如果输出缓冲区中还有未传输的数据,则将传递到目标主机。
3)SHUT_RDWR:同时断开 I/O 流。相当于分两次调用 shutdown(),其中一次以 SHUT_RD 为参数,另一次以 SHUT_WR 为参数。
close() 和 shutdown() 的区别:
确切地说,close() 用来关闭套接字,将套接字描述符从内存清除,之后再也不能使用该套接字,与C语言中的 fclose() 类似。应用程序关闭套接字后,与该套接字相关的连接和缓存也失去了意义,TCP协议会自动触发关闭连接的操作。
shutdown() 用来关闭连接,而不是套接字,不管调用多少次 shutdown(),套接字依然存在,直到调用 close() 将套接字从内存清除。调用 close() 关闭套接字时,或调用 shutdown() 关闭输出流时,都会向对方发送 FIN 包。FIN 包表示数据传输完毕,计算机收到 FIN 包就知道不会再有数据传送过来了。
默认情况下,close() 会立即向网络中发送FIN包,不管输出缓冲区中是否还有数据,而shutdown() 会等输出缓冲区中的数据传输完毕再发送FIN包。也就意味着,调用 close() 将丢失输出缓冲区中的数据,而调用 shutdown() 不会。
通过域名获取IP地址
客户端中直接使用IP地址会有很大的弊端,一旦IP地址变化(IP地址会经常变动),客户端软件就会出现错误。而使用域名会方便很多,注册后的域名只要每年续费就永远属于自己的,更换IP地址时修改域名解析即可,不会影响软件的正常使用。
域名仅仅是IP地址的一个助记符,目的是方便记忆,通过域名并不能找到目标计算机,通信之前必须要将域名转换成IP地址。
gethostbyname() 函数可以完成这种转换,它的原型为:
struct hostent *gethostbyname(const char *hostname);
hostname 为主机名,也就是域名。使用该函数时,只要传递域名字符串,就会返回域名对应的IP地址。返回的地址信息会装入 hostent 结构体,该结构体的定义如下:
struct hostent{ char *h_name; //official name char **h_aliases; //alias list int h_addrtype; //host address type int h_length; //address lenght char **h_addr_list; //address list }
从该结构体可以看出,不只返回IP地址,还会附带其他信息,我们只需关注最后一个成员 h_addr_list。下面是对各成员的说明:
1)h_name:官方域名(Official domain name)。官方域名代表某一主页,但实际上一些著名公司的域名并未用官方域名注册。
2)h_aliases:别名,可以通过多个域名访问同一主机。同一IP地址可以绑定多个域名,因此除了当前域名还可以指定其他域名。
3)h_addrtype:gethostbyname() 不仅支持 IPv4,还支持 IPv6,可以通过此成员获取IP地址的地址族(地址类型)信息,IPv4 对应 AF_INET,IPv6 对应 AF_INET6。
4)h_length:保存IP地址长度。IPv4 的长度为4个字节,IPv6 的长度为16个字节。
5)h_addr_list:这是最重要的成员。通过该成员以整数形式保存域名对应的IP地址。对于用户较多的服务器,可能会分配多个IP地址给同一域名,利用多个服务器进行均衡负载。
hostent 结构体变量的组成如下图所示:
下面简单运用一下该函数:
#include <iostream> #include <netdb.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> using namespace std; int main() { char hostname[100]; cin >> hostname; struct hostent *host = gethostbyname(hostname); char **pc; if(host) { //official name if(host -> h_name) cout << "official name:" << '\t' << host -> h_name << endl; //alias list cout << "alias list:" << endl; pc = host -> h_aliases; while(*pc != NULL) { cout << '\t' << *pc++ << endl; } //host address type and address length if(host -> h_addrtype == AF_INET) cout << "host address type: ipv4\t"<< host -> h_length << "-byte" << endl; else if(host -> h_addrtype == AF_INET6) cout << "host address type: ipv6\t" << host -> h_length << "-byte" << endl; //address list pc = host -> h_addr_list; cout << "address list:" << endl; while(*pc != NULL) { cout << '\t' << inet_ntoa(*(struct in_addr*)pc++) << endl; } } else cout << "ERROR" << endl; }