前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >网络编程 - Linux Socket编程

网络编程 - Linux Socket编程

作者头像
开源519
发布于 2023-03-09 07:07:56
发布于 2023-03-09 07:07:56
10.1K00
代码可运行
举报
文章被收录于专栏:开源519开源519
运行总次数:0
代码可运行

Linux Socket编程


目录

  • 前言
  • Socket的功能
  • Socket基础
    • Socket类型
    • 基本结构
    • 基本转换函数
  • 基本Socket使用
  • TCP Socket实例
  • UDP Socket实例
  • 疑难问题记录
  • 总结

前言

  socket(套接字)是网络编程编程的一种技巧。通过socket不仅可以实现跨进程通信,还可以实现跨主机的网络通信。使用这种技术,就可以实现全国各地的通讯。例如:深圳的一台电脑接收来自北京一台电脑发来的信息。   本篇不涉及太底层的网络原理,仅说明socket的基本使用方法。主要参考《Linux网络编程》。本篇源码获取方式见文底小字。

Socket的功能

  socket是通过标准的UNIX文件描述符和其他的程序通讯的一个方法。其可以实现同一主机不同进程间的通信;也可以实现不同主机间的通信。

Socket基础

Socket类型

  套接字有三种类型:流式套接字(SOCK_STREAM),数据报套接字(SOCK_DGRAM)和原始套接字。

流式套接字(SOCK_STREAM)   流式的套接字可以提供可靠的、面向连接的通讯流。如果你通过流式套接字发送了顺序的数据:"1"、"2"。那么数据到达远程时候的顺序也是"1"、"2"。

面向连接的Socket工作流程

数据报套接字(SOCK_DGRAM)   数据报套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证可靠,无差错。

原始套接字(SOCK_RAM)   原始套接字主要用于一些协议的开发,可以进行比较底层的操作。它功能强大,但是没有上面介绍的两种套接字使用方便,一般的程序也涉及不到原始套接字。

基本结构

  • struct sockaddr 此结构用于存储套接字地址。在需要通信地址时,此结构体会被用到,例如connect()
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct sockaddr {
    unsigned short sa_family; /* address族, AF_xxx */
    char sa_data[14];         /* 14 bytes的协议地址 */
};

sa_family为指定协议族,通常使用到的有AF_INET、AF_UNIX等。 sa_data为不同协议族通信时必要的数据。例如,sa_family为AF_INET时,sa_data要传IP地址和端口号。

  • struct sockaddr_in sockaddr_in是用于存储AF_INET的套接字地址,其中in就代表Inet。   介绍sockaddr时,说到在使用AF_INET需要传IP和端口号,但并不知道要将IP和端口号填到sockaddr的哪个地方。于是,设计了sockaddr_in,定义出地址和端口号成员。在使用时只需要填充sockaddr_in,传参时强转为sockaddr即可(两个结构体大小一致)。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct sockaddr_in {
    short int sin_family;        /* Internet地址族 */
    unsigned short int sin_port; /* 端口号 */
    struct in_addr sin_addr;     /* Internet地址 */
    unsigned char sin_zero[8];   /* 添0(和struct sockaddr一样大小) */
};
  • struct in_addr in_addrsockaddr_in成员,用于存储4个字节的IP地址。需要注意的是,此值填写时需要按照网络字节来填充,可以通过一些转换函数完成。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct in_addr {
    unsigned long s_addr;
};
  • struct sockaddr_un sockaddr_un是用于存储AF_UNIX的套接字地址,推测un就代表UNIX(没有求证)。   和上述类似,当使用AF_UNIX时,需要填充AF_UNIX的地址结构体sockaddr_un,然后传参时强转为sockaddr
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct sockaddr_un
{
    uint8_t sun_len;
    sa_family_t sun_family;  /* AF_LOCAL */
    char sun_path[104];      /* null-terminated pathname */
};

基本转换函数

网络字节序转换   上文描述填写IP和端口时要注意网络字节序,否则可能连接不到指定的设备。系统提供了如下几种函数方便转换:

  • htons()—— “Host to Network Short” 主机字节顺序转换为网络字节顺序(对无符号短型进行操作 4 bytes)
  • htonl()—— “Host to Network Long” 主机字节顺序转换为网络字节顺序(对无符号长型进行操作 8 bytes)
  • ntohs()—— “Network to Host Short” 网络字节顺序转换为主机字节顺序(对无符号短型进行操作 4 bytes)
  • ntohl()—— “Network to Host Long” 网络字节顺序转换为主机字节顺序(对无符号长型进行操作 8 bytes)

IP地址转换

  • inet_addr()—— 把一个用数字和点表示 IP 地址的字符串转换成网络字节序的无符号长整型。
  • inet_ntoa()—— “Network to ASCII” 将网络字节序的长整型转换成字符串。

inet_ntoa()返回一个字符指针,它指向一个定义在函数 inet_ntoa() 中的 static 类型字符串。所以每次调用 inet_ntoa(),都会改变最后一次调用 inet_ntoa() 函数时得到的结果。

基本Socket使用

  Linux同时支持面向连接和不连接类型的套接字。在面向连接的通讯中服务器和客户机在交换数据之前先要建立一个连接;在不连接通讯中数据被作为信息的一部分被交换。   无论那一种方式,服务器总是最先启动,把自己绑定(Banding)在一个套接字上,然后侦听信息。

socket主要使用到如下函数:

  • socket()函数 —— 创建套接字。
  • bind()函数 —— 绑定socket地址信息。(Inet需要传入IP、端口;Unix 需要传入路径)
  • connect()函数 —— 连接指定服务器套接字。
  • listen()函数 —— 服务器监听连接上的套接字客户端。
  • accept()函数 —— 接受远程客户端套接字,会获取到远程连接客户端的地址信息。(阻塞接口)
  • send()函数/recv()函数 —— 连接的流式套接字进行通讯的函数。
  • sendto()函数/recvfrom()函数 —— 非连接的数据报套接字进行通讯的函数。
  • close()函数 —— 关闭套接字描述符所表示的连接。
  • shutdown()函数 —— 指定关闭套接字的方式。
  • setsockopt()函数/getsockopt()函数 —— 套接字设置项的设置和获取。
  • getpeername()函数 —— 取得一个已经连接上的套接字的远程信息。
  • getsockname()函数 —— 取得本地主机的信息。
  • gethostbyname()函数 —— 通过域名获取IP地址。
  • gethostbyaddr()函数 —— 通过一个IPv4的地址来获取主机信息。
  • getprotobyname()函数 —— 获得网络协议名。

TCP Socket实例

  TCP Socket可以理解为Inet使用流式套接字,为保证通讯稳定而采用TCP协议。其优点在于可靠、稳定。

TCP Socket 服务端

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void DealClientMsgThread(int fd)
{
    while(1)
    {
        char buf[1024] = {0};
        int ret = read(fd, buf, sizeof(buf));
        if (ret > 0) {
            TSVR_LOG("# RECEIVE: %s.\n", buf);

            // response dstAddr msg
            char ack[20] = {0};
            snprintf(ack, sizeof(ack), "Ack [%ld]", strlen(buf));
            write(fd, ack, strlen(ack));
        } else {
            break;
        }
    }
    
    TSVR_LOG("Disconnect.\n");
}

int main(int argc, char *argv[])
{
    int i = 0;
    std::thread threads[MAX_NUM_THREAD];

    if (argc != 2)
    {
        TSVR_LOG("usage ./tcp_server 8080.\n");
        return -1;
    }

    string port = argv[1];

    int sockFd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockFd == -1)
    {
        TSVR_LOGE("socket failed. (%s)\n", strerror(errno));
        return -1;
    }

    int op = 1;
    if (setsockopt(sockFd, SOL_SOCKET, SO_REUSEADDR, &op, sizeof(op)) < 0) {
        TSVR_LOGE("setsockopt failed. (%s)\n", strerror(errno));
        goto exit;
    }

    struct sockaddr_in myAddr;
    bzero(&myAddr, sizeof(myAddr));
    myAddr.sin_family = AF_INET;
    myAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    myAddr.sin_port = htons(atoi(port.c_str()));
    if (bind(sockFd, (struct sockaddr *)&myAddr, sizeof(myAddr)) == -1) {
        TSVR_LOGE("bind failed. (%s)", strerror(errno));
        goto exit;
    }

    if (listen(sockFd, MAX_SIZE_BACKLOG) == -1) {
        TSVR_LOGE("listen failed. (%s)\n", strerror(errno));
        goto exit;
    }

    while(1)
    {
        // accept
        struct sockaddr_in dstAddr;
        socklen_t addrSize = (socklen_t)sizeof(dstAddr);
        int conFd = accept(sockFd, (struct sockaddr *)&dstAddr, &addrSize);
        if (conFd < 0) {
            TSVR_LOGE("accept failed. (%s)\n", strerror(errno));
            continue;
        }
        
        if (i < MAX_NUM_THREAD)
        {
            threads[i] = std::thread(DealClientMsgThread, conFd);
            threads[i].detach();
            i++;
        } else {
            TSVR_LOG("The number of threads reaches max(%d).\n", MAX_NUM_THREAD);
        }
    }

exit:
    close(sockFd);
    return 0;
}

TCP Socket 客户端

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int main(int argc, char *argv[])
{
    int op = 1024;

    if (argc != 3)
    {
        TCLT_LOG("usage ./tcp_client <ip> <port>.\n");
        return -1;
    }

    string ipAddr = argv[1];
    string port = argv[2];

    int sockFd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockFd == -1)
    {
        TCLT_LOGE("socket failed. (%s)\n", strerror(errno));
        return -1;
    }

    if (setsockopt(sockFd, SOL_SOCKET, SO_RCVBUF, &op, sizeof(op)) < 0) {
        TCLT_LOGE("setsockopt failed. (%s)\n", strerror(errno));
        close(sockFd);
        return -1;
    }

    if (setsockopt(sockFd, SOL_SOCKET, SO_SNDBUF, &op, sizeof(op)) < 0) {
        TCLT_LOGE("setsockopt failed. (%s)\n", strerror(errno));
        close(sockFd);
        return -1;
    }

    struct sockaddr_in dstAddr;
    dstAddr.sin_family = AF_INET;
    dstAddr.sin_addr.s_addr = inet_addr(ipAddr.c_str());
    dstAddr.sin_port = htons(atoi(port.c_str()));
    // Linux TCP repeat connect directly return EISCONN.
    if (   connect(sockFd, (struct sockaddr*)&dstAddr, sizeof(struct sockaddr_in)) < 0 
        && errno != EISCONN) 
    {
        TCLT_LOGE("connect %s:%d failed. (%s)\n", ipAddr.c_str(), atoi(port.c_str()), strerror(errno));
        close(sockFd);
        return -1;
    }

    thread rTh([&]() {
        char recvBuf[1024] = {0};
        TCLT_LOG("Start write thread.\n");

        while(1)
        {
            int ret = read(sockFd, recvBuf, sizeof(recvBuf));
            if (ret > 0) {
                TCLT_LOG("# RECEIVE: %s.\n", recvBuf);
            } else {
                break;
            }
        }
    });
    
    thread wTh([&]() {
        TCLT_LOG("Start read thread.\n");

        while(1)
        {
            char sndBuf[] = "Hello World";
            int ret = write(sockFd, sndBuf, strlen(sndBuf));
            if (ret > 0)
            {
                TCLT_LOG("# SEND > %s.\n", sndBuf);
            } else {
                break;
            }
            sleep(2);
        }
    });
    
    rTh.join();
    wTh.join();

    return 0;
}

代码是很简单的TCP通讯处理,为方便理解,不做过多封装。

UDP Socket实例

  UDP Socket可以理解为Inet使用数据报套接字,为了快速通讯,客户端与服务端约定采用的UDP的套接字通讯。

UDP Socket 服务端

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int main(int argc, char *argv[])
{
    struct sockaddr_in dstAddr;
    socklen_t addrSize = sizeof(struct sockaddr_in);

    if (argc != 2)
    {
        USVR_LOG("usage ./udp_server 8080.\n");
        return -1;
    }
    
    string port = argv[1];

    int sockFd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockFd == -1)
    {
        USVR_LOGE("socket failed. (%s)\n", strerror(errno));
        return -1;
    }
    
    int op = 1;
    if (setsockopt(sockFd, SOL_SOCKET, SO_REUSEADDR, &op, sizeof(op)) < 0) {
        USVR_LOGE("setsockopt failed. (%s)\n", strerror(errno));
        goto exit;
    }

    struct sockaddr_in myAddr;
    bzero(&myAddr, sizeof(struct sockaddr_in));
    myAddr.sin_family = AF_INET;
    myAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    myAddr.sin_port = htons(atoi(port.c_str()));
    if (bind(sockFd, (struct sockaddr *)&myAddr, sizeof(struct sockaddr_in)) < 0) {
        USVR_LOGE("bind failed. (%s)\n", strerror(errno));
        goto exit;
    }

    while(1)
    {
        char buf[1024] = {0};
        int ret = recvfrom(sockFd, buf, sizeof(buf), 0, 
                    (struct sockaddr *)&dstAddr, &addrSize);

        if (ret > 0) {
            char ack[20] = {0};

            snprintf(ack, sizeof(ack), "Ack [%ld]", strlen(buf));
            USVR_LOG("# RECEIVE: %s.\n", buf);
            sendto(sockFd, ack, strlen(ack), 0, 
                    (struct sockaddr *)&dstAddr, addrSize);

        } else {
            USVR_LOG("recvfrom failed. (%s)\n", strerror(errno));
            break;
        }
    }

exit:
    close(sockFd);
    return 0;
}

UDP Socket 客户端

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        UCLT_LOG("usage ./udp_client <ip> <port>.\n");
        return -1;
    }

    string ipAddr = argv[1];
    string port = argv[2];

    int sockFd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockFd == -1)
    {
        UCLT_LOGE("socket failed. (%s)\n", strerror(errno));
        return -1;
    }

    int op = 1024;
    if (setsockopt(sockFd, SOL_SOCKET, SO_RCVBUF, &op, sizeof(op)) < 0) {
        UCLT_LOGE("setsockopt failed. (%s)\n", strerror(errno));
        close(sockFd);
        return -1;
    }

    struct sockaddr_in dstAddr;
    bzero(&dstAddr, sizeof(struct sockaddr_in));
    dstAddr.sin_family = AF_INET;
    dstAddr.sin_addr.s_addr = inet_addr(ipAddr.c_str());
    dstAddr.sin_port = htons(atoi(port.c_str()));

    thread wTh([&]() {
        UCLT_LOG("Start write thread.\n");

        while(1)
        {
            char sndBuf[] = "Hello World";
            int ret = sendto(sockFd, sndBuf, strlen(sndBuf), 0, 
                        (struct sockaddr *)&dstAddr, sizeof(struct sockaddr_in));

            if (ret > 0) {
                UCLT_LOG("# SEND: %s.\n", sndBuf);
            } else {
                break;
            }

            sleep(2);
        }
    });
    
    thread rTh([&]() {
        struct sockaddr_in dstAddr;
        UCLT_LOG("Start read thread.\n");

        while(1)
        {
            char recvBuf[1024] = {0};
            socklen_t addrSize = sizeof(struct sockaddr_in);
            bzero(&dstAddr, addrSize);
            int ret = recvfrom(sockFd, recvBuf, sizeof(recvBuf), 0, 
                        (struct sockaddr *)&dstAddr, &addrSize);

            if (ret > 0)
            {
                UCLT_LOG("# RECEIVE: %s.\n", recvBuf);
            } else {
                break;
            }
        }
    });
    
    wTh.join();
    rTh.join();
    return 0;
}

疑难问题记录

  • TCP服务端和客户端如何精确地检测到对方下线或异常断开? ① 接收函数是阻塞的,当对方断开,接收函数会返回异常。 ② 通过错误码和信号判断,当一端异常断开,另一端会收到SIGPIPE信号,再通过getsockopt查询各个套接字确认哪一个断开。
  • UDP 如何保证通讯数据稳定 UDP通讯因为不需要连接,从而比TCP通讯更快,但是由于其不确保数据是否安全到达,从而显得不靠谱。为了实现通讯靠谱,需要在应用层建立一套机制验证数据准确性,建立失误重传的约定。

总结

  • socket的实现非常优秀,将复杂的网络通信,封装成简单的socket的接口。使用者不用过多考虑TCP、UDP以及其他较底层的网络概念,而快速的实现一套网络通讯的流程。
  • 本文仅列举了socket用于inet地址族的例程,其还可以用于UNIX域的进程间通讯。
  • 网络编程非常有趣,能够实现天南海北之间的通讯,让远距离的人与人、人与物或者物与物之间产生联系,很有意思!

最后

用心感悟,认真记录,写好每一篇文章,分享每一框干货。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-01-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 开源519 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
60 个让程序员崩溃的瞬间,太TM真实了
原文出处: https://zhuanlan.zhihu.com/p/47066521
KEN DO EVERTHING
2019/01/17
5670
22个令程序员流下了没有技术泪水的瞬间
❈ 师长感慨波多,Java为啥总是学不完呢。请看下面这些搞笑瞬间... ❈ 1. 公司的实习生找Bug 2. 在调试时,将断点设置在错误的位置 3. 当我有一个很棒的调试想法时 4. 偶然间看到自
用户1257393
2018/12/28
5260
21个令程序员泪流满面的瞬间
转载自公众号:程序员最幽默 「1」 公司实习生找 Bug 「2」 在调试时,将断点设置在错误的位置 「3」 当我有一个很棒的调试想法时 「4」 偶然间看到自己多年前写的代码 「5」 当我第一次启动我的单元测试时 「6」 数据库的Delete语句忘了使用限定词where... 「7」 明明是个小bug 但就是死活修不好...... 「8」 当我尝试调整生产数据库中的一些东西时 「9」 好像真的没人发现我产品里的bug...... 「10」 下班前我还有一项任务没有完成 「11」 产品还没测试直接投入生产
用户1634449
2018/10/18
4210
程序员常见的口头禅,哈哈哈哈~
0、这个昨天还是正常的。 1、试试三连 你刷新下试试。 你换个浏览器试试。 你电脑重启下试试。 2、在我的电脑上明明可以,你清理下缓存。 3、这个实现不了 4、注释 谁的代码,注释都不写。 我先完成功
芋道源码
2021/01/25
6020
程序员常见的口头禅,哈哈哈哈~
疯了!程序员崩溃的40个瞬间!!!
说到程序员,在外界眼里,他们是掌控代码的大神,他们是改变世界的王者。其实程序员的工作不容易,不信,就来看看程序员崩溃的各种瞬间——
CDA数据分析师
2020/02/24
5590
20张图表达程序员的心酸
程序员最幽默(ID:humor1024)编译配字 「1」 被老板委派接手刚刚离职同事的项目... 「2」 当他们要求我测试所有应用功能时 「3」 准备下班的时候,测试又提bug过來了… 「4」 使用
Python数据科学
2018/12/19
4230
20张图表达程序员的心酸
45张令程序员泪流满面的趣图
20 实习生将他的代码交给高级开发人员,高级开发人员反手就是一个Code Review
IT阅读排行榜
2019/04/25
2.4K0
45张令程序员泪流满面的趣图
21个令程序员泪流满面的瞬间
来源:程序员最幽默 本文约多图,建议阅读8分钟。 本文为你生动形象地展示程序员的世界。 1. 明明我只修改了一行代码... 2. 千万不要随便乱动旧项目 3. 提交了错误的分支 4. 断点调
数据派THU
2018/10/25
6580
21个令程序员泪流满面的瞬间
程序员幽默搞笑GIF:这些痛,只有程序员懂…… 有图有真相
张培跃 ID:laozhangsishu 不止于前端 关注 “代码上线后又追加了新特性” “在生产环境做 hotfix” “刚调稳定的系统,公司叕空降了一位架构师,叕要重构现有系统…… ”
用户1272076
2019/03/26
2.4K0
程序员幽默搞笑GIF:这些痛,只有程序员懂…… 有图有真相
增长黑客和程序员沟通指南:避开这8个坑爹瞬间
其实,国内外增长专家认为,增长黑客更多的是一种思维(mindset),而非技术(technical)。写代码属于锦上添花的技能,而不是必要条件。
纯洁的微笑
2019/05/06
3470
增长黑客和程序员沟通指南:避开这8个坑爹瞬间
内涵! 程序员才懂的20张动图...
转自:Hollis 1. Java VS C++ 2. 功能先上了再说 3.高级开发人员作为一个团队进行编程 4. 调试CSS 5. 编译错误:括号不匹配 6.高级开发人员重构代码 7. 看实习生编码
挖数
2019/05/14
1.3K0
内涵! 程序员才懂的20张动图...
2月幽默集合(上)
每天进步一丢丢,连接梦与想 转自:程序员最幽默(humor1024) 大家对于成功码农的定义是什么? 标准程序员专用拖鞋 @IT程序猿 微博网友评论: @霜黯暮秋:家乐福我买过20块的 @麦田1900
KEN DO EVERTHING
2019/03/12
6320
2月幽默集合(上)
程序员,这是不是你的逗逼日常?
合并分支 Git merge用来做分支合并,将其他分支中的内容合并到当前分支中。 我们看看程序员合并代码的情形: 重构代码 代码重构就是在不改变软件系统外部行为的前提下,改善它的内部结构。 我们看看程
程序员的酒和故事
2018/03/12
6780
程序员,这是不是你的逗逼日常?
程序员那些牛逼闪闪的禁术,看到第二条我就忍不住哈哈哈哈哈哈哈哈
在和产品经理进行需求对峙时,一本正经地把不好/不想实现的功能通过玄学等方式口述出来,让产品经理当场懵逼,知难而退。
IT阅读排行榜
2018/08/16
2620
程序员那些牛逼闪闪的禁术,看到第二条我就忍不住哈哈哈哈哈哈哈哈
在和产品经理进行需求对峙时,一本正经地把不好/不想实现的功能通过玄学等方式口述出来,让产品经理当场懵逼,知难而退。
IT派
2018/07/30
4350
程序员那些牛逼闪闪的禁术,看到第二条我就忍不住哈哈哈哈哈哈哈哈
这些痛,只有程序员懂…
刚调稳定的系统,公司叕空降了一位架构师,叕要重构现有系统…… 当程序员听客户说还在用 IE 时 代码进入循环分支后 项目交接,客户要求开发现场显示,结果…… 新手程序员第一次做项目的过程 零错误零警
前端教程
2018/03/29
5150
这些痛,只有程序员懂…
程序员编程障碍
编程效率障碍No.1:会议 最常见的抱怨是打断开发人员编码思绪的会议。如果老板信任该程序员,就会要求他们时不时地去那间数周甚至数年昏昏暗暗的会议室闲聊有关细节。尽管程序员通常归咎于是管理人员毁了
企鹅号小编
2018/01/09
6090
程序员编程障碍
二十个令程序员泪流满面的瞬间,最后一条让你泣不成声
本文列举了20个令程序员泪流满面的瞬间,包括调试时无法修复bug、意外走入会议室、发现多年前写的代码、多线程调试、结对编程、调试低效、被PM催改需求、试图退出vim、清理旧代码、编译错误等。这些瞬间反映了程序员的幽默和无奈,以及他们所面临的挑战和压力。
企鹅号小编
2018/01/03
5100
二十个令程序员泪流满面的瞬间,最后一条让你泣不成声
21个令程序员泪流满面的瞬间
「1」 它刚才明明运行得好好的 「2」 客户给我看他的需求文档 「3」 拼命地在日志中搜索应用程序崩溃的原因 「4」 学习新语言的过程:一看就会,一学就废 「5」 本地测试成功,演示各种到位,交付给
@超人
2021/07/29
3100
21个令程序员泪流满面的瞬间
毁灭程序员的15个障碍
毁灭程序员的15个障碍 会议,什么都不懂的经理,生产效率指标——这就是你和下一个伟大软件之间的天堑。 昨天必须得发布产品。用户争闹和咆哮某个缺失的功能。老板的老板说,我们最好迅速行动起来否则就炒我们的鱿鱼。感觉一切都有心无力。 没有人满意开发人员这种已经“竭尽全力”改变世界的速度,每个人都希望代码像消防水管里的水一样能够源源不断地流出来,但没有人愿意提供给开发人员更好地完成工作的条件。正如那个想要我们昨天就完成工作的老板,他不愿意雇佣更多的人,不愿意购买速度更快的机器,也不愿意做任何其他可以让程序员专注于编
用户1289394
2018/02/27
6790
毁灭程序员的15个障碍
推荐阅读
相关推荐
60 个让程序员崩溃的瞬间,太TM真实了
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验