#5 Redis 创始人 antirez 提供的一个 C 语言示例:聊天服务器

2023-11-04

代码地址:https://github.com/antirez/smallchat
视频讲解:https://www.youtube.com/watch?v=eT02gzeLmF0

作者给做前端开发的朋友写的一个网络编程示例,只有 200 多行纯 C 代码,没有第三方依赖,使用了 select 多路复用实现命令行聊天。
可以用来复习一下 C 编程。

简化部分注释后:

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <assert.h>
#include <sys/select.h>

/* ============================ 数据结构 ================================= */

#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.
};

/* 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.
    struct client *clients[MAX_CLIENTS]; // Clients are set in the corresponding
                                         // slot of their socket descriptor.
};

struct chatState *Chat; // Initialized at startup.

/* ======================== 网络方法 ========================== */

/* Create a TCP socket listening to 'port' ready to accept connections. */
int createTCPServer(int port) {
    int s, yes = 1;
    struct sockaddr_in sa;

    if ((s = socket(AF_INET, SOCK_STREAM, 0)) == -1) return -1;
    setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); // Best effort.

    memset(&sa,0,sizeof(sa));
    sa.sin_family = AF_INET;
    sa.sin_port = htons(port);
    sa.sin_addr.s_addr = htonl(INADDR_ANY);

    if (bind(s,(struct sockaddr*)&sa,sizeof(sa)) == -1 ||
        listen(s, 511) == -1)
    {
        close(s);
        return -1;
    }
    return s;
}

/* 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. */
    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. */
    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. */
int acceptClient(int server_socket) {
    int s;

    while(1) {
        struct sockaddr_in sa;
        socklen_t slen = sizeof(sa);
        s = accept(server_socket,(struct sockaddr*)&sa,&slen);
        if (s == -1) {
            if (errno == EINTR)
                continue; /* Try again. */
            else
                return -1;
        }
        break;
    }
    return s;
}

/* 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;
}

/* ====================== 聊天服务实现 ======================== */

/* Create a new client bound to 'fd'. This is called when a new client
 * connects. As a side effect updates the global Chat state. */
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. */
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);
}

/* 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);
    if (Chat->serversock == -1) {
        perror("Creating listening socket");
        exit(1);
    }
}

/* 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. */
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);
    }
}

/* 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. */
        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). */
        tv.tv_sec = 1; // 1 sec timeout
        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. */
        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. */
                        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. */
                            char msg[256];
                            int msglen = snprintf(msg, sizeof(msg),
                                "%s> %s", c->nick, readbuf);
                            if (msglen >= (int)sizeof(msg))
                                msglen = sizeof(msg)-1;
                            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;
}

#4 为什么 C/C++ 不可替代

2022-10-31

英文原文:https://levelup.gitconnected.com/why-modern-alternative-languages-never-replace-c-c-cbf0afc5f1dc
中文翻译:https://mp.weixin.qq.com/s/tr69w96gOO7ia81ttilgVA

总结一下:

  1. C/C 是优秀的系统级编程语言,无数基础设施是用 C/C 开发的
  2. C/C++ 可以完全控制机器,开发者拥有最大的灵活性
  3. C/C++ 性能非常好
  4. C/C++ 学术友好,简单,高效,直接(没有封装得很抽象)
  5. 所有操作系统提供 C API,其他语言也都提供了和 C 交互的方式

#3 c const

2021-10-06

C 常量

c 常量的两种实现方法:

  1. define
  2. const 关键字

const 关键字

  • 常量 const int a / int const a
    a 是一个 int 类型常量
  • 常量指针 const int *a / int const *a
    a 是一个指针,指向一个 int 类型常量
  • 指针常量 int *const a
    a 是一个指针常量(指针类型(int *)的常量), 指向的位置不能修改
  • 指向常量的指针常量 const int *const a
    同上,不过更进一步,指向位置是一个常量(const int),不能更改

问题

int main()
{
    const int a = 1;

    int *ptr = (int *)&a;
    *ptr = 2;
    printf("a = %d\n", a);

    return 0;
}

如果用 gcc 编译会输出:a = 2, g++ 的话,会输出 a = 1, WHY? 为什么 C 允许修改常量,是不是一个 BUG?

原因

参考网上诸多资料,我的理解如下:

  1. C 会给 const 常量正常分配内存,const 只会在编译时起到静态分析的作用(限定常量不能作为左值)。
  2. C++ 则是将 const 常量放在符号表,如果用指针指向这个常量,会新分配内存地址,操作也是操作的这个内存空间,符号表中存储的值不受影响。
  3. 符号表在内存布局中的那一部分?

参考资料与拓展阅读

#2 阮一峰的 C 语言教程学习

2021-09-07
  1. https://wangdoc.com/clang/
  2. GitHub, https://github.com/wangdoc/clang-tutorial

目录

  • 1. 简介
    历史,标准,Hello World
  • 2. 基本语法
    基本语法,printf 格式化
  • 3. 变量
  • 4. 运算符
  • 5. 流程控制
  • 6. 数据类型
  • 整型的不同进制表示
  • limits.h 中的极限值
  • 关于布尔型,int, _Bool(C99, int 别名), bool/true/false (stdbool.h)
  • 字面量后缀 (l, ll, u, f)
  • 类型转换
  • 可移植类型 (stdint.h)
  • 7. 指针
  • 8. 函数
  • 9. 数组
  • 10. 字符串
  • 11. 内存管理
  • 12. struct 结构
  • 13. typedef 命令
  • 14. Union 结构
  • 15. Enum 结构
  • 16. 预处理器
  • 17. I/O 函数
  • 18. 文件操作
  • 19. 变量说明符
  • 20. 多文件项目
  • 21. 命令行环境
  • 22. 多字节字符
  • 23. 标准库
  • 23.1. assert.h
  • 23.2. ctype.h
  • 23.3. errno.h
  • 23.4. float.h
  • 23.5. inttypes.h
  • 23.6. iso646.h
  • 23.7. limits.h
  • 23.8. locale.h
  • 23.9. math.h
  • 23.10. signal.h
  • 23.11. stdint.h
  • 23.12. stdlib.h
  • 23.13. stdio.h
  • 23.14. string.h
  • 23.15. time.h
  • 23.16. wchar.h
  • 23.17. wctype.h

关键字

// 数据类型 12
short   int         long    double  float
char    struct      union   enum    typedef
signed  unsigned

// 变量 6
auto    register    extern  const   volatile
static

// 函数 2
void    return

// 控制语句 11
if      else    for     continue    while  do
switch  case    default goto        break

// 其他 1
sizeof

数据类型

  • 整数:short, int, long, long long
  • signed, unsigned
  • 浮点数:float, double
  • 字符:char, char *
#include <stdio.h>

int main(){
    printf("size of int:         %2ld\n", sizeof(int));
    printf("size of short:       %2ld\n", sizeof(short));
    printf("size of long:        %2ld\n", sizeof(long));
    printf("size of long long:   %2ld\n", sizeof(long long));
    printf("size of float:       %2ld\n", sizeof(float));
    printf("size of double:      %2ld\n", sizeof(double));
    printf("size of long double: %2ld\n", sizeof(long double));
    printf("size of char:        %2ld\n", sizeof(char));
}
$ uname -a
Linux dell 5.11.0-34-generic #36-Ubuntu SMP Thu Aug 26 19:22:09 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

$ gcc --version
gcc (Ubuntu 10.3.0-1ubuntu1) 10.3.0
Copyright (C) 2020 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ gcc /tmp/test.c -o /tmp/test && /tmp/test
size of int:          4
size of short:        2
size of long:         8
size of long long:    8
size of float:        4
size of double:       8
size of long double: 16
size of char:         1

#1 Visual C++

2016-11-07

VC6

Microsoft Visual C(简称Visual C、MSVC、VC或VC)是微软公司的C开发工具,具有集成开发环境,可提供编辑C语言,C以及C/CLI等编程语言。

MS 的 C/C++ 开发工具,继承了他们家的 Win32API,MFC,.NET 等编程框架。

最早是 MSC (Microsoft C);
后来添加 C 支持,改名叫 MS C/C;
再后来,GUI 编程兴起,改名 Visual C,简称 MSVC;
再后来,.NET 框架出来了,改名 Visual C
.NET (Visual C .NET 2002);
再后来,05 年,又给改回去了 (Visual C
2005);
现在最新版本已经到了 Visual C++ 2015。

版本历史

Version Time Notes
MSC 1.0 1983
MSC 7.0 1989
MS C/C++ 7.0 1992 c++, mfc
VC++ 1.0 1993 16bit 版本
VC++ 1.52C 1993 16bit 最后版本
VC++ 1.0 1993 32bit 版本, 重置了版本号
VC++ 2.0 Win95 发布
VC++ 4.0 1995
VC++ 5.0 1997
VC++ 6.0 1998
Visual C++ .NET 2002 2002 7.0
Visual C++ .NET 2003 2003 7.1
Visual C++ 2005 2005 8.0, 开始支持 64bit
Visual C++ 2008 2007 9.0
Visual C++ 2010 2010 10.0
Visual C++ 2012 2012 11.0
Visual C++ 2013 2013 12.0
Visual C++ 2015 2015 14.0
Visual C++ 2017 2017 14.10
Visual C++ 2019 2019 14.20

PS: 大学学 C++ 的时候就是用的 VC6。

VC2015

  • C99
    2013 才开始支持,到 2015,基本上实现了 C99。
    PS: C11 就不要想了。
  • C14
    微软在 C
    领域还是很有发言权的。

.NET

Managed C++

Managed Extensions for C, C托管扩展

微软对 C 做了一个语法拓展(属性和关键字),便于将 C 编译成 .NET 平台中间代码。

非托管代码(没有改写)可以与托管代码进行很好的互操作。

但是由于引入了大量新的代码,代码整体可读性降低了。

C++/CLI

经过多年工程实践,微软为 Managed C 进行了大量改进。然后可能是标准化(ECMA-372)的缘故,新版本改名叫 C/CLI。
VC2005 开始使用 C/CLI 代替 Managed C

根据文档(如何将 C++/CLI 项目移植到 .NET Core)来看,C++/CLI 项目就算移植到 .NET Core 也只能运行于 Windows。原因不明。

参考资料与拓展阅读