源码学习:smallchat

7 篇文章 0 订阅

源码:https://github.com/antirez/smallchat
可用于学习网络通信,源码很小。

考虑先基于第一版进行分析:
https://github.com/antirez/smallchat/blob/Episode-1/smallchat.c

先给出readme文件的翻译:
Smallchat
TLDR: 这只是一个我为几位朋友提供的编程示例。我上传了一个视频到我的YouTube频道,放大了代码,以查看从这个如此简单且故意破碎的示例中可以学到什么。将会有更多的视频和改进,详见本README文件末尾。

现在,让我来讲述完整的故事:

昨天我与我的几位朋友交谈,他们大多是前端开发者,对系统编程有些距离。我们在回忆IRC的旧时光。不可避免地,我说:编写一个非常简单的IRC服务器是每个人都应该做的经历(我向他们展示了我用TCL编写的实现,我很震惊我18年前就写了它:时间过得真快)。像那样的程序中有一些非常有趣的部分。一个进程执行多路复用,获取客户端状态,并在客户端有新数据时尝试快速访问这种状态,等等。

但后来讨论演变,我想,我来展示一个非常简单的C语言示例给你们看。你能写出最小的聊天服务器是什么样的?首先,要真正简化,我们不应该要求任何正式的客户端。即使不太完善,它应该能够与telnet或nc(netcat)一起使用。服务器的主要操作只是接收一些聊天行并将其发送给所有其他客户端,有时被称为扇出操作。但是,这将需要一个适当的readline()函数,然后进行缓冲等等。我们希望它更简单:让我们使用内核缓冲区来欺骗,假装我们每次都从客户端接收到一个完整形式的行(在实践中,这种假设通常成立,所以事情有点能用)。

好吧,通过这些技巧,我们可以实现一个聊天,甚至可以让用户在只有200行代码的情况下设置他们的昵称(当然要删除空格和注释)。由于我将这个小程序作为给我的朋友的示例,我决定也将其推送到Github上。

未来的工作
在接下来的几天里,我将继续修改这个程序以发展它。不同的演变步骤将根据我关于编写系统软件系列的YouTube剧集中的更改进行标记。这是我的计划(可能会改变,但大致上这是我想要涵盖的内容):

  1. 实现读写缓冲。
  2. 避免线性数组,使用字典数据结构来保存客户端状态。
  3. 编写一个适当的客户端:能够处理异步事件的行编辑。
  4. 实现频道。
  5. 从select(2)切换到更高级的API。
  6. 为聊天实现简单的对称加密。

不同的更改将由一个或多个YouTube视频进行介绍。完整的提交历史将保存在这个存储库中。
/
下面是第一版的源码,后面会与第二版源码进行对比说明:
smallchat.c – 读取客户端输入,发送给所有其他连接的客户端。
数据结构:最小化,且简单。实际核心就一个chatState对象Chat。

#define MAX_CLIENTS 1000 // This is actually the higher file descriptor. 客户端总数限制
#define SERVER_PORT 7711 //服务器端口
/* This structure represents a connected client. There is very little
 * info about it: the socket descriptor and the nick name, if set, otherwise
 * the first byte of the nickname is set to 0 if not set.
 * The client can set its nickname with /nick <nickname> command. */
struct client {
    int fd;     // Client socket. 客户端的套接字描述符,用于与客户端通信。
    char *nick; // Nickname of the client. 客户名,默认为0
};
/* This global structure encasulates the global state of the chat. 封装聊天程序的全局状态信息。*/
struct chatState {
    int serversock;     // Listening server socket. 服务器的监听套接字,用于接受客户端连接请求。
    int numclients;     // Number of connected clients right now.当前连接的客户端数量。
    int maxclient;      // The greatest 'clients' slot populated.最大的 'clients' 槽位被占用的索引。
    struct client *clients[MAX_CLIENTS]; // Clients are set in the corresponding slot of their socket descriptor.客户端数组,用于存储连接到服务器的客户端信息。每个客户端通过其套接字描述符在数组中找到对应的位置。
};
struct chatState *Chat; // Initialized at startup.指向 struct chatState 结构的指针,用于表示整个聊天程序的全局状态。在程序启动时会初始化该结构。

api:最基本的socket
我调整一下学习顺序,从低到高,按被调用顺序看

先看内存申请。其实只是为了在内存不足时,先输出错误信息,并正常退出返回1,避免崩溃

/* We also define an allocator that always crashes on out of memory: you
 * will discover that in most programs designed to run for a long time, that
 * are not libraries, trying to recover from out of memory is often futile
 * and at the same time makes the whole program terrible. */
void *chatMalloc(size_t size) {
    void *ptr = malloc(size);
    if (ptr == NULL) {
        perror("Out of memory");
        exit(1);
    }
    return ptr;
}
/* Also aborting realloc(). */
void *chatRealloc(void *ptr, size_t size) {
    ptr = realloc(ptr,size);
    if (ptr == NULL) {
        perror("Out of memory");
        exit(1);
    }
    return ptr;
}

初始化全局的唯一的数据结构对象,创建tcp服务器

/* Create a TCP socket lisetning to 'port' ready to accept connections. 
   创建一个监听 'port' 端口的 TCP 套接字,准备接受连接。*/
int createTCPServer(int port) {
    int s=-1;//socket文件句柄fd
    int yes = 1;
    struct sockaddr_in sa;

    // 创建一个套接字
    // AF_INET是IPv4  SOCK_STREAM是sock_type 流式套接字
    //int __socket (int domain, int type, int protocol)这个找的似乎不对 TODO
    if ((s = socket(AF_INET, SOCK_STREAM, 0)) == -1) return -1;

    // 设置套接字选项,允许地址重用(SO_REUSEADDR)。
    //这是一个尽力而为的设置(Best effort),允许在套接字关闭后立即重新绑定相同的地址SO_REUSEADDR。
    //int __setsockopt (int fd, int level, int optname, const void *optval, socklen_t len) ?TODO
    setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); // Best effort.

    memset(&sa,0,sizeof(sa));
    sa.sin_family = AF_INET; // 地址族为 AF_INET IPv4
    sa.sin_port = htons(port); // 设置端口号,htons是大小尾处理函数,s是short 16bit
    sa.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有可用的网络接口,htonl是大小尾处理函数,l是long 32bit
    //#define	INADDR_ANY		((in_addr_t) 0x00000000)

    // 尝试绑定套接字到指定的地址和端口,以及开始监听连接请求
    //int __bind  (int fd, __CONST_SOCKADDR_ARG addrarg, socklen_t len)
    //int listen (int fd, int backlog) 511指定最大待处理连接请求队列的长度
    if (bind(s,(struct sockaddr*)&sa,sizeof(sa)) == -1 ||
        listen(s, 511) == -1){
        close(s);//int __close (int fd)
        return -1;
    }
    return s;
}
/* Allocate and init the global stuff. */
void initChat(void) {
    Chat = chatMalloc(sizeof(*Chat));
    memset(Chat,0,sizeof(*Chat));
    /* No clients at startup, of course. */
    Chat->maxclient = -1;
    Chat->numclients = 0;
    /* Create our listening socket, bound to the given port. This
     * is where our clients will connect. */
    Chat->serversock = createTCPServer(SERVER_PORT); //创建tcp服务器
    if (Chat->serversock == -1) {
        perror("Creating listening socket");
        exit(1);
    }
}

ipv4 ipv6常量

#define PF_INET		2	/* IP protocol family.  */
#define PF_INET6	10	/* IP version 6.  */
/* Set the specified socket in non-blocking mode, with no delay flag. 
   将指定的套接字设置为非阻塞模式,并关闭延迟标志。*/
int socketSetNonBlockNoDelay(int fd) {
    int flags, yes = 1;
    /* Set the socket nonblocking.设置套接字为非阻塞模式。
     * Note that fcntl(2) for F_GETFL and F_SETFL can't be interrupted by a signal. 
     * 请注意,fcntl(2) 用于 F_GETFL 和 F_SETFL 的操作不会被信号中断。*/
    //fcntl 函数获取套接字的当前属性标志(flags)
    if ((flags = fcntl(fd, F_GETFL)) == -1) return -1;
    if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) return -1;//设置为非阻塞模式
    /* This is best-effort. No need to check for errors. 
       这是尽力而为的操作,不需要检查错误。*/
    //关闭 TCP 套接字的延迟标志(TCP_NODELAY)
    setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &yes, sizeof(yes));
    return 0;
}
/* If the listening socket signaled there is a new connection ready to be accepted, 
 * we accept(2) it and return -1 on error or the new client socket on success. 
 * 如果监听套接字指示有一个新的连接准备好被接受,
 * 我们使用 accept(2) 函数接受该连接,并在错误时返回 -1,成功时返回新的客户端套接字。*/
int acceptClient(int server_socket) {
    int s;
    while(1) {
        struct sockaddr_in sa;
        socklen_t slen = sizeof(sa);
        // 使用 accept 函数尝试接受客户端连接
        s = accept(server_socket,(struct sockaddr*)&sa,&slen);
        if (s == -1) {
	        // 如果 accept 函数返回 -1,可能是由于以下原因之一:
	        // - EINTR:系统调用被信号中断,可以尝试再次接受。
	        // - 其他错误:发生其他错误,无法接受连接。
            if (errno == EINTR)
                continue; /* Try again. */
            else
                return -1;
        }
        break;// 成功接受连接,跳出循环。
    }
    return s;
}

/* ====================== Small chat core implementation 聊天核心实现========================
 * Here the idea is very simple: we accept new connections, read what clients
 * write us and fan-out (that is, send-to-all) the message to everybody
 * with the exception of the sender. And that is, of course, the most
 * simple chat system ever possible.
 * 这里的思想非常简单:我们接受新的连接,读取客户端发来的消息,
 * 并将消息广播(即发送给所有人),除了消息发送者。
 * 这是一种最简单的聊天系统实现。
 * =========================================================================== */

/* Create a new client bound to 'fd'. This is called when a new client connects. 
 * As a side effect updates the global Chat state. 
 * 创建一个绑定到 'fd' 的新客户端。这在新客户端连接时被调用。
 * 作为副作用,更新全局 Chat 状态。*/
struct client *createClient(int fd) {
    char nick[32]; // Used to create an initial nick for the user.用户昵称
    int nicklen = snprintf(nick,sizeof(nick),"user:%d",fd);
    struct client *c = chatMalloc(sizeof(*c));
    socketSetNonBlockNoDelay(fd); // Pretend this will not fail.假装这不会失败。
    c->fd = fd;//客户端文件句柄
    c->nick = chatMalloc(nicklen+1);//客户端昵称
    memcpy(c->nick,nick,nicklen);
    assert(Chat->clients[c->fd] == NULL); // This should be available.
    Chat->clients[c->fd] = c;//存储客户端指针
    /* We need to update the max client set if needed. 
    如果需要,我们需要更新最大客户端集合*/
    if (c->fd > Chat->maxclient) Chat->maxclient = c->fd;
    Chat->numclients++;
    return c;
}

/* Free a client, associated resources, and unbind it from the global state in Chat. 
   释放客户端、相关资源,并从 Chat 的全局状态中解绑。*/
void freeClient(struct client *c) {
    free(c->nick);
    close(c->fd);
    Chat->clients[c->fd] = NULL;
    Chat->numclients--;
    if (Chat->maxclient == c->fd) {
        /* Ooops, this was the max client set. Let's find what is the new highest slot used. 
           这是最大的客户端集合。让我们找出是 新的最高使用槽位是什么。*/
        int j;
        for (j = Chat->maxclient-1; j >= 0; j--) {
            if (Chat->clients[j] != NULL) Chat->maxclient = j;
            break;
        }
        if (j == -1) Chat->maxclient = -1; // We no longer have clients.我们不再有客户端。
    }
    free(c);
}
/* Send the specified string to all connected clients but the one having as socket descriptor 'excluded'. 
   If you want to send something to every client just set excluded to an impossible socket: -1. 
   将指定的字符串发送给所有连接的客户端,但不发送给具有套接字描述符 'excluded' 的客户端。
   如果要发送消息给每个客户端,请将 'excluded' 设置为一个不可能的套接字:-1。*/
void sendMsgToAllClientsBut(int excluded, char *s, size_t len) {
    for (int j = 0; j <= Chat->maxclient; j++) {
        if (Chat->clients[j] == NULL ||
            Chat->clients[j]->fd == excluded) continue;
        /* Important: we don't do ANY BUFFERING. We just use the kernel socket buffers.
           If the content does not fit, we don't care. This is needed in order to keep this program simple. 
           重要提示:我们不进行任何缓冲。我们只使用内核的 套接字缓冲区。
           如果内容不适合,我们不关心。这是为了保持这个程序的简单性所必需的。*/
        write(Chat->clients[j]->fd,s,len);
        //将指定的字符串消息 s 写入客户端的套接字,以将消息发送给客户端。
        //需要注意的是,该函数没有进行任何消息缓冲,它直接使用内核的套接字缓冲区,
        //如果消息内容超过了套接字缓冲区的容量,可能会导致部分消息被丢弃。
    }
}
/* The main() function implements the main chat logic:  实现聊天的主要逻辑:
 * 1. Accept new clients connections if any. 接受新的客户端连接(如果有的话)。
 * 2. Check if any client sent us some new message. 检查是否有客户端发送新消息。
 * 3. Send the message to all the other clients. 将消息发送给所有其他客户端。*/
int main(void) {
    initChat();
    while(1) {
        fd_set readfds;// 文件描述符集合,用于监视可读事件。
        struct timeval tv;// 用于设置超时时间。
        int retval;

        FD_ZERO(&readfds);
        /* When we want to be notified by select() that there is activity? 
           If the listening socket has pending clients to accept or if any other client wrote anything. 
           我们想要被 select() 通知有活动的时刻是什么?
           如果监听套接字有待接受的新客户端连接,或者任何其他客户端写了新消息。*/
        FD_SET(Chat->serversock, &readfds);
        
        //遍历客户端指针
        for (int j = 0; j <= Chat->maxclient; j++) {
            if (Chat->clients[j]) FD_SET(j, &readfds);
        }

        /* Set a timeout for select(), see later why this may be useful in the future (not now). 
           为 select() 设置一个超时时间,稍后可以看到为什么这可能有用(现在不是)。*/
        tv.tv_sec = 1; // 1 sec timeout 1 秒超时
        tv.tv_usec = 0;

        /* Select wants as first argument the maximum file descriptor in use plus one.
           It can be either one of our clients or the server socket itself. 
           select() 需要的第一个参数是当前使用的最大文件描述符加一。
           它可以是我们的任何一个客户端或服务器套接字本身。*/
        int maxfd = Chat->maxclient;
        if (maxfd < Chat->serversock) maxfd = Chat->serversock;
        retval = select(maxfd+1, &readfds, NULL, NULL, &tv);
        if (retval == -1) {
            perror("select() error");
            exit(1);
        } else if (retval) {
            /* If the listening socket is "readable", it actually means there are new clients connections pending to accept. 
              如果监听套接字是 "可读" 的,实际上意味着有待接受的新客户端连接。*/
            if (FD_ISSET(Chat->serversock, &readfds)) {
                int fd = acceptClient(Chat->serversock);
                struct client *c = createClient(fd);
                /* Send a welcome message. 发送欢迎消息*/
                char *welcome_msg =
                    "Welcome to Simple Chat! "
                    "Use /nick <nick> to set your nick.\n";
                write(c->fd,welcome_msg,strlen(welcome_msg));
                printf("Connected client fd=%d\n", fd);
            }

            /* Here for each connected client, check if there are pending data the client sent us. 
               对于每个已连接的客户端,检查是否有待处理的数据。*/
            char readbuf[256];
            for (int j = 0; j <= Chat->maxclient; j++) {
                if (Chat->clients[j] == NULL) continue;
                if (FD_ISSET(j, &readfds)) {
                    /* Here we just hope that there is a well formed message waiting for us.
                       But it is entirely possible that we read just half a message.
                       In a normal program that is not designed to be that simple, 
                       we should try to buffer reads until the end-of-the-line is reached.
                       这里我们希望有一条格式良好的消息等待我们。
                       但完全有可能我们只读取了消息的一部分。
                       在一个正常程序中,不是设计得如此简单的情况下,
                       我们应该尝试缓冲读取,直到达到行尾。  */
                    int nread = read(j,readbuf,sizeof(readbuf)-1);

                    if (nread <= 0) {
                        /* Error or short read means that the socket was closed.
                           错误或短读取表示套接字已关闭。*/
                        printf("Disconnected client fd=%d, nick=%s\n",
                            j, Chat->clients[j]->nick);
                        freeClient(Chat->clients[j]);
                    } else {
                        /* The client sent us a message. 
                           We need to relay this message to all the other clients in the chat.
                           客户端发送了一条消息。我们需要转发这条消息给聊天室中的其他客户端。  */
                        struct client *c = Chat->clients[j];
                        readbuf[nread] = 0;

                        /* If the user message starts with "/", we process it as a client command. 
                           So far only the /nick <newnick> command is implemented. 
                           如果用户消息以 "/" 开头,我们将其处理为客户端命令。
                           目前只实现了 /nick <newnick> 命令。*/
                        if (readbuf[0] == '/') {
                            /* Remove any trailing newline. 移除任何尾随的换行符。*/
                            char *p;
                            p = strchr(readbuf,'\r'); if (p) *p = 0;
                            p = strchr(readbuf,'\n'); if (p) *p = 0;
                            /* Check for an argument of the command, after the space.
                               检查命令后面是否有参数,空格后面是参数。 */
                            char *arg = strchr(readbuf,' ');
                            if (arg) {
                                *arg = 0; /* Terminate command name. */
                                arg++; /* Argument is 1 byte after the space. */
                            }

                            if (!strcmp(readbuf,"/nick") && arg) {
                                free(c->nick);
                                int nicklen = strlen(arg);
                                c->nick = chatMalloc(nicklen+1);
                                memcpy(c->nick,arg,nicklen+1);
                            } else {
                                /* Unsupported command. Send an error. 不支持的命令。发送错误消息。*/
                                char *errmsg = "Unsupported command\n";
                                write(c->fd,errmsg,strlen(errmsg));
                            }
                        } else {
                            /* Create a message to send everybody (and show on the server console)
                               in the form: nick> some message.
                               创建要发送给所有其他客户端(并显示在服务器控制台上)的消息,
                               格式为:nick> 某条消息 */
                            char msg[256];
                            int msglen = snprintf(msg, sizeof(msg),
                                "%s> %s", c->nick, readbuf);
                            printf("%s",msg);

                            /* Send it to all the other clients. 将消息发送给所有其他客户端*/
                            sendMsgToAllClientsBut(j,msg,msglen);
                        }
                    }
                }
            }
        } else {
            /* Timeout occurred. We don't do anything right now,
               but in general this section can be used to wakeup periodically even if there is no clients activity.
               超时发生。目前我们什么都不做,但通常情况下,这个部分可以用于定期唤醒,
               即使没有客户端活动也会发生。 */
        }
    }
    return 0;
}

以下TCPConnect函数是第二版新增函数,归属于chatlib文件

/* Create a TCP socket and connect it to the specified address.
 * 创建一个TCP套接字并连接到指定的地址和端口
 * On success the socket descriptor is returned, otherwise -1.
 * 如果连接成功,返回连接的套接字描述符;否则,返回-1。
 * If 'nonblock' is non-zero, the socket is put in nonblocking state
 * 如果为非零值,将套接字设置为非阻塞模式。
 * and the connect() attempt will not block as well, but the socket may not be immediately ready for writing. 
 * 连接尝试不会阻塞,但套接字可能不会立即准备好写入。
 * */
int TCPConnect(char *addr, int port, int nonblock) {
    int s;//套接字描述符
    int retval = -1;//返回值
    struct addrinfo hints, *servinfo, *p;//声明地址信息结构体和指针
    char portstr[6]; /* Max 16 bit number string length. 存放端口号字符串的数组 */
    snprintf(portstr,sizeof(portstr),"%d",port);//将端口号转换为字符串
    memset(&hints,0,sizeof(hints));
    hints.ai_family = AF_UNSPEC;//地址族为未指定,可以是IPv4或IPv6
    hints.ai_socktype = SOCK_STREAM;//套接字类型为流式套接字(TCP)
     //获取地址信息
    if (getaddrinfo(addr,portstr,&hints,&servinfo) != 0) return -1;
	//遍历获取到的地址信息列表
    for (p = servinfo; p != NULL; p = p->ai_next) {
        /* Try to create the socket and to connect it.尝试创建套接字并连接,
         * If we fail in the socket() call, or on connect(), we retry with the next entry in servinfo.
         * 如果在socket()或connect()调用中失败,将在servinfo中的下一个条目中重试 */
        if ((s = socket(p->ai_family,p->ai_socktype,p->ai_protocol)) == -1)
            continue;//建套接字失败则继续下一个地址信息
        /* Put in non blocking state if needed. 如果需要,将套接字设置为非阻塞状态*/
        if (nonblock && socketSetNonBlockNoDelay(s) == -1) {
            close(s);//关闭套接字
            break;
        }
        /* Try to connect. 尝试连接套接字*/
        if (connect(s,p->ai_addr,p->ai_addrlen) == -1) {
            /* If the socket is non-blocking, it is ok for connect() to return an EINPROGRESS error here. 
             * 如果套接字是非阻塞的,那么在此处返回EINPROGRESS错误是可以接受的
             *   EINPROGRESS = 0x40000024, Operation now in progress */
            if (errno == EINPROGRESS && nonblock) return s;
            /* Otherwise it's an error. 否则,连接出错*/
            close(s);
            break;
        }
        /* If we ended an iteration of the for loop without errors, 
         * we have a connected socket. Let's return to the caller.
         * 如果在for循环的一次迭代结束时没有出现错误,说明已经成功连接。
         * 返回连接的套接字描述符给调用者。 */
        retval = s;
        break;
    }
    freeaddrinfo(servinfo);//释放地址信息资源
    return retval; /* Will be -1 if no connection succeded. */
}

glibc中的相关定义

struct addrinfo{
  int ai_flags;			/* Input flags.  */
  int ai_family;		/* Protocol family for socket.套接字的协议族, 如 IPv4 或 IPv6 */
  int ai_socktype;		/* Socket type.  */
  int ai_protocol;		/* Protocol for socket. 如 IPPROTO_TCP 或 IPPROTO_UDP */
  socklen_t ai_addrlen;		/* Length of socket address. 套接字地址的长度。通常是 uint32。*/
  struct sockaddr *ai_addr;	/* Socket address for socket. 套接字地址结构的指针 */
  char *ai_canonname;		/* Canonical name for service location. 服务位置的规范名称 */
  struct addrinfo *ai_next;	/* Pointer to next in list.  */
};
/* Structure describing a generic socket address. 描述通用套接字地址的结构体 */
struct sockaddr  {
    __SOCKADDR_COMMON (sa_);	/* Common data: address family and length. 公共数据:地址族和长度。通常是一个 uint16。 */
    char sa_data[14];		/* Address data.  */
  };
  #define	__SOCKADDR_COMMON(sa_prefix) \
  sa_family_t sa_prefix##family  //typedef unsigned short int sa_family_t;套接字地址的协议族。

__socket_type

/* Types of sockets.  */
enum __socket_type{
  SOCK_STREAM = 1,		/* Sequenced, reliable, connection-based byte streams.
                           有序、可靠的、基于连接的字节流套接字,用于创建 TCP 套接字。*/
#define SOCK_STREAM SOCK_STREAM
  SOCK_DGRAM = 2,		/* Connectionless, unreliable datagrams of fixed maximum length. 
                           无连接、不可靠的、具有固定最大长度的数据报套接字,用于创建 UDP 套接字。*/
  SOCK_RAW = 3,			/* Raw protocol interface. 原始协议接口套接字,通常用于实现自定义协议或访问底层网络层。 */
  SOCK_RDM = 4,			/* Reliably-delivered messages. 可靠传递的消息套接字 */
  SOCK_SEQPACKET = 5,		/* Sequenced, reliable, connection-based, datagrams of fixed maximum length.
                               有序、可靠、基于连接的、具有固定最大长度的数据报套接字 */
  SOCK_DCCP = 6,		/* Datagram Congestion Control Protocol. 数据报拥塞控制协议套接字,用于创建 DCCP 套接字 */
  SOCK_PACKET = 10,		/* Linux specific way of getting packets at the dev level.  
                           For writing rarp and other similar things on the user level. 
				           Linux 特定方式的获取设备层级别的数据包的套接字,通常用于底层网络操作。*/
  /* Flags to be ORed into the type parameter of socket and socketpair and used for the flags parameter of paccept. 
     用于 socket 和 socketpair 的 type 参数和 paccept 的 flags 参数的标志。
     可以与套接字类型参数进行 OR 操作。 */
  SOCK_CLOEXEC = 0x400000,	/* Atomically set close-on-exec flag for the new descriptor(s).  
                               为新描述符原子性地设置关闭时执行标志。(即在执行 exec 调用时自动关闭描述符。)*/
  SOCK_NONBLOCK = 0x004000	/* Atomically mark descriptor(s) as non-blocking. 
                               原子性地将描述符标记为非阻塞 */
};

IP protocols

/* Standard well-defined IP protocols.  */
enum {
    IPPROTO_IP = 0,	   /* Dummy protocol for TCP. 虚拟协议,用于表示 TCP 协议 */
    IPPROTO_ICMP = 1,	   /* Internet Control Message Protocol. ICMP用于网络通信中的错误和控制消息。 */
    IPPROTO_IGMP = 2,	   /* Internet Group Management Protocol. 用于管理 IP 多播组成员关系*/
    IPPROTO_IPIP = 4,	   /* IPIP tunnels (older KA9Q tunnels use 94). IPIP 协议,用于 IP 隧道。 */
    IPPROTO_TCP = 6,	   /* Transmission Control Protocol.  用于可靠的、面向连接的数据传输。*/
    IPPROTO_EGP = 8,	   /* Exterior Gateway Protocol. 用于路由器之间的信息交换。 */
    IPPROTO_PUP = 12,	   /* PUP protocol. 一种早期的网络协议。 */
    IPPROTO_UDP = 17,	   /* User Datagram Protocol. 用于无连接的数据传输。 */
    IPPROTO_IDP = 22,	   /* XNS IDP protocol. 用于 Xerox Network Systems */
    IPPROTO_TP = 29,	   /* SO Transport Protocol Class 4. 传输层协议类 4。 */
    IPPROTO_DCCP = 33,	   /* Datagram Congestion Control Protocol. 用于拥塞控制。 */
    IPPROTO_IPV6 = 41,     /* IPv6 header.  IPv6 头部协议。 */
    IPPROTO_RSVP = 46,	   /* Reservation Protocol. 用于资源预留。 */
    IPPROTO_GRE = 47,	   /* General Routing Encapsulation. 用于通用路由封装。 */
    IPPROTO_ESP = 50,      /* encapsulating security payload. 用于数据加密和认证。 */
    IPPROTO_AH = 51,       /* authentication header. 用于数据认证。 */
    IPPROTO_MTP = 92,	   /* Multicast Transport Protocol. 用于多播传输。 */
    IPPROTO_BEETPH = 94,   /* IP option pseudo header for BEET. BEETPH(BEET IP 选项伪头)协议。 */
    IPPROTO_ENCAP = 98,	   /* Encapsulation Header.用于封装数据。  */
    IPPROTO_PIM = 103,	   /* Protocol Independent Multicast. 用于多播通信。 */
    IPPROTO_COMP = 108,	   /* Compression Header Protocol. 用于数据压缩。 */
    IPPROTO_L2TP = 115,	   /* Layer 2 Tunnelling Protocol. 用于第二层隧道。 */
    IPPROTO_SCTP = 132,	   /* Stream Control Transmission Protocol. 用于流控制传输。 */
    IPPROTO_UDPLITE = 136, /* UDP-Lite protocol. 用于轻量级 UDP。 */
    IPPROTO_MPLS = 137,    /* MPLS in IP. 用于 MPLS 封装在 IP 中。 */
    IPPROTO_ETHERNET = 143, /* Ethernet-within-IPv6 Encapsulation. 用于 IPv6 封装以太网数据。 */
    IPPROTO_RAW = 255,	   /* Raw IP packets. 原始 IP 数据包协议,用于处理未经处理的 IP 数据包。 */
    IPPROTO_MPTCP = 262,   /* Multipath TCP connection. 用于多路径 TCP 连接。 */
    IPPROTO_MAX
  };

第一版只有一个smallchat.c文件,似乎对应着第二版的smallchat-server.c中的内容和chatlib,
smallchat-client.c是第二版补齐的。以下分析客户端源码:

setRawMode设置或取消终端的原始(raw)模式

/* Raw mode: 1960 magic shit. 原始模式:1960年代的黑魔法。*/
int setRawMode(int fd, int enable) {
    /* We have a bit of global state (but local in scope) here.
     * This is needed to correctly set/undo raw mode. */
     * 这里有一些全局状态(但是在范围内是局部的)。
     * 这是为了正确设置/取消原始模式而需要的。*/
    static struct termios orig_termios; // Save original terminal status here.保存原始终端状态
    static int atexit_registered = 0;   // Avoid registering atexit() many times.避免多次注册atexit()
    static int rawmode_is_set = 0;      // True if raw mode was enabled.如果启用了原始模式,则为真。
    struct termios raw;
    /* If enable is zero, we just have to disable raw mode if it is currently set. 
       如果 enable 为零,我们只需在当前设置时禁用原始模式。 */
    if (enable == 0) {
        /* Don't even check the return value as it's too late.甚至不要检查返回值,因为现在为时已晚。 */
        if (rawmode_is_set && tcsetattr(fd,TCSAFLUSH,&orig_termios) != -1)
            rawmode_is_set = 0;
        return 0;
    }

    /* Enable raw mode. 启用原始模式*/
    if (!isatty(fd)) goto fatal;//检查文件描述符是否关联到终端
    if (!atexit_registered) {
        atexit(disableRawModeAtExit);//注册退出时调用的函数,确保在程序结束时会禁用原始模式
        atexit_registered = 1;
    }
    if (tcgetattr(fd,&orig_termios) == -1) goto fatal;//获取当前终端属性并保存为原始状态

    raw = orig_termios;  /* modify the original mode 使用原始状态创建一个修改后的副本*/
    /* input modes: no break, no CR to NL, no parity check, no strip char, no start/stop output control. 
     * 输入模式:无断点,无CR到NL,无奇偶校验,无剥离字符,无启动/停止输出控制。*/
    raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
    /* output modes - do nothing. We want post processing enabled so that \n will be automatically translated to \r\n. 
     * 输出模式 - 无操作。我们希望启用后期处理,以便 \n 会自动转换为 \r\n。*/
    // raw.c_oflag &= ...
    /* control modes - set 8 bit chars 控制模式 - 设置 8 位字符*/
    raw.c_cflag |= (CS8);
    /* local modes - choing off, canonical off, no extended functions,
     * but take signal chars (^Z,^C) enabled. 
     * 本地模式 - 关闭回显,关闭规范模式,无扩展功能, 但启用信号字符(^Z,^C)。*/
    raw.c_lflag &= ~(ECHO | ICANON | IEXTEN);
    /* control chars - set return condition: min number of bytes and timer.
     * We want read to return every single byte, without timeout. 
     * 控制字符 - 设置返回条件:最小字节数和计时器。
     * 我们希望 read 返回每个单字节,而不带有超时。*/
    raw.c_cc[VMIN] = 1; raw.c_cc[VTIME] = 0; /* 1 byte, no timer */

    /* put terminal in raw mode after flushing 在刷新后将终端设置为原始模式*/
    if (tcsetattr(fd,TCSAFLUSH,&raw) < 0) goto fatal;
    rawmode_is_set = 1;
    return 0;

fatal:
    errno = ENOTTY;
    return -1;
}
/* At exit we'll try to fix the terminal to the initial conditions. */
void disableRawModeAtExit(void) {
    setRawMode(STDIN_FILENO,0);//退出程序时,恢复STDIN的raw模式
}

以下为最小化的行编辑器

/* ============================================================================
 * Mininal line editing.最小化的行编辑器
 * ========================================================================== */
void terminalCleanCurrentLine(void) {
    write(fileno(stdout),"\e[2K",4);//清除当前行
}
void terminalCursorAtLineStart(void) {
    write(fileno(stdout),"\r",1);//将光标移到行首
}

#define IB_MAX 128
struct InputBuffer {//输入缓冲区结构体,包含数据的缓冲区和当前长度
    char buf[IB_MAX];       // Buffer holding the data.
    int len;                // Current length.
};

/* inputBuffer*() return values: 返回值定义*/
#define IB_ERR 0        // Sorry, unable to comply.无法执行
#define IB_OK 1         // Ok, got the new char, did the operation, ...成功,获得新字符,执行了相应操作
#define IB_GOTLINE 2    // Hey, now there is a well formed line to read. 嘿,现在有一行完整的输入可供读取

/* Append the specified character to the buffer. 将指定的字符附加到缓冲区*/
int inputBufferAppend(struct InputBuffer *ib, int c) {
    if (ib->len >= IB_MAX) return IB_ERR; // No room.如果缓冲区已满,则返回错误
    ib->buf[ib->len] = c;
    ib->len++;
    return IB_OK;
}

/* Hide the line the user is typing. 隐藏用户正在输入的行。*/
void inputBufferHide(struct InputBuffer *ib) {
    (void)ib; // Not used var, but is conceptually part of the API.
    terminalCleanCurrentLine();
    terminalCursorAtLineStart();
}
/* Show again the current line. Usually called after InputBufferHide(). 
   再次显示当前行。通常在 InputBufferHide() 之后调用。*/
void inputBufferShow(struct InputBuffer *ib) {
    write(fileno(stdout),ib->buf,ib->len);
}

/* Process every new keystroke arriving from the keyboard. As a side effect
 * the input buffer state is modified in order to reflect the current line
 * the user is typing, so that reading the input buffer 'buf' for 'len' bytes will contain it. 
 * 处理从键盘输入的每个新按键。作为副作用,
 * 修改输入缓冲区状态以反映用户正在输入的当前行,
 * 以便读取输入缓冲区 'buf' 的 'len' 字节将包含它。*/
int inputBufferFeedChar(struct InputBuffer *ib, int c) {
    switch(c) {
    case '\n':
        break;          // Ignored. We handle \r instead.忽略换行符,我们处理回车符
    case '\r':
        return IB_GOTLINE; //发送信息
    case 127:           // Backspace. 删除一个字符
        if (ib->len > 0) {
            ib->len--;
            inputBufferHide(ib);
            inputBufferShow(ib);
        }
        break;
    default:
        if (inputBufferAppend(ib,c) == IB_OK)//行编辑
            write(fileno(stdout),ib->buf+ib->len-1,1);
        break;
    }
    return IB_OK;
}
/* Reset the buffer to be empty. 将缓冲区重置为空*/
void inputBufferClear(struct InputBuffer *ib) {
    ib->len = 0;
    inputBufferHide(ib);
}

以下为客户端主程序,使用前面的行编辑器api与server端交互:
关键点就是通过select函数轮询文件(net和stdin)进行接收数据
通过write写到TCP文件s,发给server,被其他客户端监听到

int main(int argc, char **argv) {
    if (argc != 3) {//检查命令行参数是否正确,需要提供服务器的主机名和端口号
        printf("Usage: %s <host> <port>\n", argv[0]);
        exit(1);
    }

    /* Create a TCP connection with the server. 创建与服务器的TCP连接*/
    int s = TCPConnect(argv[1],atoi(argv[2]),0);
    if (s == -1) {
        perror("Connecting to server");
        exit(1);
    }

    /* Put the terminal in raw mode: this way we will receive every
     * single key stroke as soon as the user types it. No buffering
     * nor translation of escape sequences of any kind. 
     * 将终端设置为原始模式:这样我们将立即接收用户键入的每个按键。
     * 没有任何形式的缓冲或转义序列的翻译。 */
    setRawMode(fileno(stdin),1);

    /* Wait for the standard input or the server socket to have some data. 
     * 等待标准输入或服务器套接字有数据*/
    fd_set readfds;
    int stdin_fd = fileno(stdin);

    struct InputBuffer ib;
    inputBufferClear(&ib);//初始化输入缓冲区结构,就是清空数据

    while(1) {
        //文件描述符集合 readfds,这个集合包含了要监视的文件描述符。
        FD_ZERO(&readfds);//清空 readfds 集合,将其所有位都设置为0。
        FD_SET(s, &readfds);//TCP //将 s 对应的位设置为1,将 s 添加到 readfds 集合中。
        FD_SET(stdin_fd, &readfds);//将 stdin_fd 对应的位设置为1,将 stdin_fd 添加到 readfds 集合中。
        int maxfd = s > stdin_fd ? s : stdin_fd;//计算 s 和 stdin_fd 中较大的文件描述符,
        //maxfd 用于 select 函数中,指定待检查的文件描述符的数量。
        int num_events = select(maxfd+1, &readfds, NULL, NULL, NULL);
        if (num_events == -1) {
            perror("select() error");
            exit(1);
        } else if (num_events) {
            char buf[128]; /* Generic buffer for both code paths. 用于两个代码路径的通用缓冲区*/
            if (FD_ISSET(s, &readfds)) {
                /* Data from the server? 从服务器接收数据?*/
                ssize_t count = read(s,buf,sizeof(buf));
                if (count <= 0) {
                    printf("Connection lost\n");
                    exit(1);
                }
                inputBufferHide(&ib);
                write(fileno(stdout),buf,count);//输出从server端收到的信息
                inputBufferShow(&ib);
            } else if (FD_ISSET(stdin_fd, &readfds)) {
                /* Data from the user typing on the terminal? 从终端用户键入的数据?*/
                ssize_t count = read(stdin_fd,buf,sizeof(buf));
                for (int j = 0; j < count; j++) {
                    int res = inputBufferFeedChar(&ib,buf[j]);
                    switch(res) {
                    case IB_GOTLINE://发送数据
                        inputBufferAppend(&ib,'\n');
                        inputBufferHide(&ib);
                        write(fileno(stdout),"you> ", 5);//写到本地界面
                        write(fileno(stdout),ib.buf,ib.len);
                        write(s,ib.buf,ib.len);//写到TCP,发到server
                        inputBufferClear(&ib);
                        break;
                    case IB_OK:
                        break;
                    }
                }
            }
        }
    }
    close(s);
    return 0;
}

  • 8
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值