TCP 连接概述( 续)
一个 TCP 客户端(Linux)
从命令行读入 主机名(或 IP 地址) 和 端口号, 尝试连接运行在该地址的服务器.
将从服务器收到的数据转发到终端, 将从终端输入的数据转发给服务器.
熟悉一下如何编写 TCP 客户端, 也可以用于测试后续编写的 TCP 服务器.
用 getaddrinfo() 解析服务器地址( 命令行参数).
调用 socket() 创建一个套接字.
在该套接字上调用 connect(), 向服务器发起连接.
getaddrinfo() -> socket() -> connect()
调用 select() 函数, 监视套接字的输入, 同时也监视终端键盘的输入.
如果终端有可读数据, 调用 send(), 通过套接字将数据发送出去.
如果套接字有可读数据, 调用 recv(), 将数据在终端显示出来.
重复这一过程, 直至套接字关闭.
- /* network.h */
- #ifndef NETWORK_H
- #define NETWORK_H
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <netinet/in.h>
- #include <arpa/inet.h>
- #include <netdb.h>
- #include <unistd.h>
- #include <errno.h>
- #endif
- /* client.tcp.c */
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <stdbool.h>
- #include "network.h"
- int main(int argc, char ** argv) {
- if (argc != 3) {
- fprintf(stderr, "Usage: %s hostname port\n", argv[0]);
- return EXIT_FAILURE;
- }
- printf("Configuring remote address ...\n");
- struct addrinfo peer_info;
- memset(&peer_info, 0, sizeof(peer_info));
- /*
- ai_socktype 置为 SOCK_STREAM, 表明连接类型为 TCP
- ai_socktype 置为 SOCK_DGRAM, 表明连接类型为 UDP
- */
- peer_info.ai_socktype = SOCK_STREAM;
- struct addrinfo * peer_addr;
- if (getaddrinfo(argv[1], argv[2], &peer_info, &peer_addr)) {
- fprintf(stderr, "getaddrinfo() failed, errno: %d, %s\n", errno, strerror(errno));
- return EXIT_FAILURE;
- }
- printf("Remote address: ");
- char addr_buff[100];
- char serv_buff[100];
- getnameinfo(
- peer_addr->ai_addr, peer_addr->ai_addrlen,
- addr_buff, sizeof(addr_buff),
- serv_buff, sizeof(addr_buff),
- NI_NUMERICHOST
- );
- printf("%s %s\n", addr_buff, serv_buff);
- printf("Creating socket ...\n");
- int socket_peer = socket(
- peer_addr->ai_family, peer_addr->ai_socktype, peer_addr->ai_protocol
- );
- if (socket_peer < 0) {
- fprintf(stderr, "socket() failed, errno: %d, %s\n" , errno, strerror(errno));
- return EXIT_FAILURE;
- }
- printf("Connecting ...\n");
- if (connect(socket_peer, peer_addr->ai_addr, peer_addr->ai_addrlen)) {
- fprintf(stderr, "connect() failed, errno: %d, %s\n", errno, strerror(errno));
- return EXIT_FAILURE;
- }
- freeaddrinfo(peer_addr);
- printf("Connected ...\n");
- printf("Input data, 'Enter' to send ...\n");
- while (true) {
- fd_set sset;
- FD_ZERO(&sset);
- FD_SET(socket_peer, &sset);
- /*
- Linux 系统, 标准输入 stdin 的文件描述符就是 0
- 将其加入集合 sset
- 也可以写成
- FD_SET(fileno(stdin), &sset);
- */
- FD_SET(0, &sset);
- struct timeval timeout;
- timeout.tv_sec = 0;
- timeout.tv_usec = 100000;
- if (select(socket_peer+1, &sset, 0, 0, &timeout) < 0) {
- fprintf(stderr, "select() failed, errno: %d, %s\n", errno, strerror(errno));
- return EXIT_FAILURE;
- }
- if (FD_ISSET(socket_peer, &sset)) {
- char data_buff[4096];
- int bytes_recv = recv(socket_peer, data_buff, sizeof(data_buff), 0);
- if (bytes_recv < 1) {
- printf("Connection closed by peer ...\n");
- break;
- }
- printf("Received: %.*s", bytes_recv, data_buff);
- printf("Received %d bytes ...\n", bytes_recv);
- }
- if (FD_ISSET(0, &sset)) {
- char data_buff[4096];
- if (fgets(data_buff, sizeof(data_buff), stdin) == 0)
- break;
- printf("Sending: %s", data_buff);
- int bytes_sent = send(socket_peer, data_buff, strlen(data_buff), 0);
- printf("Sent %d bytes ...\n", bytes_sent);
- }
- }
- printf("Closing socket ...\n");
- close(socket_peer);
- printf("Finished ...\n");
- return EXIT_SUCCESS;
- }
一个 TCP 服务器(Linux)
微服务的思想是把一个巨大的编程问题切分成很多个较小的子系统, 这些子系统可以通过网络互相通信.
比如, 程序需要将字符串格式化, 这个功能不必自己编写, 可以连接到提供该功能的服务, 从而保持程序结构的简单.
编写一个简单的服务, 将字符串的字符转换成大写形式.
绑定 监听地址和端口 到 套接字, 进行监听.
getaddrinfo() -> socket() -> bind() -> listen()
调用 select(), 监视套接字是否有新连接和新数据.
当有新连接请求时, 调用 accept().
将所有已建立的连接放入 fd_set, 传递给随后调用的 select() 函数.
这样, 我们就能知道哪些连接会在调用 recv() 时进入阻塞状态.
我们只需服务那些不会被阻塞的连接.
等到 recv() 返回数据, 就调用 toupper() 处理收到的数据, 再将处理结果返回给客户端.
- /* toupper.server.tcp.c */
- #include <stdio.h>
- #include <stdlib.h>
- #include <ctype.h>
- #include <string.h>
- #include <stdbool.h>
- #include "network.h"
- int main(int argc, char ** argv) {
- printf("Configuring local address ...\n");
- struct addrinfo info_local;
- memset(&info_local, 0, sizeof(info_local));
- info_local.ai_family = AF_INET6;
- info_local.ai_socktype = SOCK_STREAM;
- info_local.ai_flags = AI_PASSIVE;
- struct addrinfo * addr_local;
- getaddrinfo(NULL, "8080", &info_local, &addr_local);
- printf("Creating socket ...\n");
- int sock_listen = socket(
- addr_local->ai_family, addr_local->ai_socktype, addr_local->ai_protocol
- );
- if (sock_listen < 0) {
- fprintf(stderr, "socket() failed, errno: %d, %s\n", errno, strerror(errno));
- return EXIT_FAILURE;
- }
- printf("Binding socket to local address ...\n");
- if (bind(
- sock_listen,
- addr_local->ai_addr, addr_local->ai_addrlen
- )) {
- fprintf(stderr, "bind() failed, errno: %d, %s\n", errno, strerror(errno));
- return EXIT_FAILURE;
- }
- freeaddrinfo(addr_local);
- printf("Listening ...\n");
- if (listen(sock_listen, 16) < 0) {
- fprintf(stderr, "listen() failed, errno: %d, %s\n", errno, strerror(errno));
- return EXIT_FAILURE;
- }
- printf("Creating socket set ...\n");
- fd_set master;
- FD_ZERO(&master);
- FD_SET(sock_listen, &master);
- int max_sock = sock_listen;
- int i, j;
- printf("Waiting for connections ...\n");
- while (true) {
- fd_set reads = master;
- if (select(max_sock+1, &reads, 0, 0, NULL) < 0) {
- fprintf(stderr, "select() failed, errno: %d, %s\n", errno, strerror(errno));
- return EXIT_FAILURE;
- }
- for (i = 1; i <= max_sock; ++i) {
- if (FD_ISSET(i, &reads)) {
- if (i == sock_listen) {
- struct sockaddr_storage addr_client;
- socklen_t client_len = sizeof(addr_client);
- int sock_client = accept(
- sock_listen, (struct sockaddr *) &addr_client, &client_len
- );
- if (sock_client < 0) {
- fprintf(stderr, "accept() failed, errno: %d, %s\n", errno, strerror(errno));
- return EXIT_FAILURE;
- }
- FD_SET(sock_client, &master);
- if (sock_client > max_sock)
- max_sock = sock_client;
- char addr_buff[100];
- getnameinfo(
- (struct sockaddr *) &addr_client, client_len,
- addr_buff, sizeof(addr_buff),
- 0, 0, NI_NUMERICHOST
- );
- printf("New connection: %s\n", addr_buff);
- }
- else {
- char data_buff[1024];
- int bytes_received = recv(i, data_buff, sizeof(data_buff), 0);
- if (bytes_received < 1) {
- FD_CLR(i, &master);
- close(i);
- continue;
- }
- for (j = 0; j < bytes_received; ++j)
- data_buff[j] = toupper(data_buff[j]);
- send(i, data_buff, bytes_received, 0);
- }
- }
- }
- }
- printf("Closing listening ...\n");
- close(sock_listen);
- printf("Finished ...\n");
- return EXIT_SUCCESS;
- }
运行程序

函数 send() 的阻塞
调用 send() 发送数据, send() 会将 待发送数据 复制到操作系统提供的外发数据缓冲区.
如果调用 send() 时正好缓冲区已满, 就会阻塞直到缓冲区有足够的空间容纳 待发送数据 为止.
但也有些时候, send() 会在应该阻塞时没有阻塞, 而是未复制完所有的待发送数据的就返回了.
比如, send() 阻塞了程序的执行, 然后程序收到来自操作系统的信号.
这时, 发送剩余的未发送数据就是调用者的责任.
函数 send() 的返回值表明了其复制数据的字节数.
为了程序的健壮性, 应该比较 send() 的返回值 和 待发送数据总的字节数.
如果 send() 的返回值较小, 意味着实际发送的数据比应发送的少.
这时, 就应该调用 select() 得到可用的套接字, 然后调用 send() 发送那些未发送的数据.
一般情况下, 操作系统提供的缓冲区都足够大, 但如果发送的数据量比较大, 就有必要检查 send() 函数的返回值.
- /*
- 通过 peer_sock 套接字发送数据 buff, buff 的字节数为 buff_len.
- 代码阻塞, 直到 buff 发送完毕, 或 有错误发生( 如节点断开连接).
- */
- int begin = 0;
- while (begin < buff_len) {
- int sent = send(peer_sock, buff+begin, buff_len-begin, 0);
- if (sent < 0) {
- /* 错误处理 */
- }
- begin += sent;
- }
如果管理着多个套接字并且不希望程序阻塞, 应该将全部可用于调用 send() 的那些套接字加入一个 fd_set 集合.
然后, 将该集合作为第三个参数传递给 select().
TCP 是一个 流协议(stream protocol)
调用 send() 发送的数据和连接另一端 recv() 收到的数据, 二者的大小可能不同.
连接的一端调用 send() 发送 20 字节的数据, 另一端调用 recv() 接收.
为了完整接收这 20 字节, 共调用 recv() 多少次, 是无法知道的.
可能一次调用就返回了全部 20 字节, 也可能第一次返回 16 字节, 第二次返回 4 字节.
所以, 一些协议要求将收到的数据先进行缓存, 直到累积到适当大小, 足以被解释处理为止.
UDP 就不是个 流协议, 其发送的包和收到的包, 内容就是相同的.