网络编程一些问题总结

简介: 网络编程一些问题总结

1、TCP三次握手、四次挥手

1.1、三次握手与全连接、半连接的关系:

1、服务器调用listen,等待客户端的连接。

2、客户端调用connect函数,发送syn包给服务器,客户端此时会变成syn_send状态。

3、服务器收到syn信号后,将新建一个连接放到syn半连接队列中,并发送syn ack信号给客户端,此时服务端变成syn_recv状态。

4、客户端收到服务端的syn ack信号,再次发送ack信号给服务端,并且客户端状态变成establish状态。

5、服务端收到ack信号后状态变成establish,并将开始放入半连接队列的那个连接移出、放到accept全连接队列中。

6、服务端调用accept()函数,从accept全连接队列中取连接,然后新建一个socket。

wireshark三次握手抓包测试方法:

wireshark选择"loopback"环回网卡还是抓包。然后打开2个NetAssist,一个客户端、一个服务器,服务器端口配置8888

抓到的TCP三次握手包如下:

1.2、四次挥手

1、客户端和服务器处于Establish正常通信状态

2、客户端调用close函数,给服务器发送FIN数据包,此时客户端变为FIN-WAIT-1状态

3、服务器接收到FIN信号,发送ACK信号给客户端,此时服务器改变状态为CLOSE_WAIT状态

4、客户端收到ACK信号后,改变状态为FIN-WAIT-2

5、服务器调用Close函数,给客户端发送FIN信号,然后服务器状态变为LAST-ACK

6、客户端收到FIN信号,给服务器发送一个ACK包,客户端将状态变为TIME-WAIT

7、服务器收到ACK信号后,将状态变为CLOSED

8、客户端状态变为TIME-WAIT

断开客户端连接,抓包如下:

1.3、TCP扩展知识

DDOS攻击:客户端只发送SYN信号,不给服务器回复ACK信号,三次握手只进行第一步,会导致服务器的syn半连接队列溢出

send()返回大于0不代表发送成功,只是代表将数据放到了内核协议栈的发送缓冲buffer中,只有当对方发送ack并且自己收到ack消息后,才表明发送真正的成功了。

客户端宕机的检测方式:发送心跳报

2、网络编程常见问题

2.1、服务器只执行到listen
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(9999);
    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
        printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
    if (listen(listenfd, 10) == -1) {
        printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }

这段代码可以支持多个客户端的连接,可以完成三次握手,但是连接只会存放到全连接队列Accept队列中,客户端所有的读写操作都是失败的。只有listen执行而无accept,也是可以完成客户端的连接的。三次握手不由应用程序管理,而是应该在执行listen之后由底层的内核协议栈执行。

listenfd为3的原因:因为stdin,stdout,stderr占据了0、1、2三个文件描述符,所以一般listenfd从3开始。

2.2、服务器执listen和accpet在一起
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(9999);
    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
        printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
    if (listen(listenfd, 10) == -1) {
        printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
    struct sockaddr_in client;
    socklen_t len = sizeof(client);
    if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
        printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
    while (1) {
        n = recv(connfd, buff, MAXLNE, 0);
        if (n > 0) {
            buff[n] = '\0';
            printf("recv msg from client: %s\n", buff);
        send(connfd, buff, n, 0);
        } else if (n == 0) {
            close(connfd);
        }

这段代码可以支持多个客户端连接,但只有第一个客户端可以正常与服务器进行通信。注意,listen是非阻塞的,而accept是阻塞的。当第一个客户端连接进来后,accept返回的时第一个连接的fd,然后进入while(1)一直执行,期间不再会调用accept函数再从Accept全连接队列中取出新的连接生成新的客户端fd,所以读写操作都是执行的第一个连接的fd的操作。

accept函数的作用:

1、从accept全连接队列中取出一个连接,如果全连接队列为空那么会阻塞等待

2、为新的连接分配一个socket fd

2.3、accept和recv都放到while(1)中
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
      printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
      return 0;
  }
  memset(&servaddr, 0, sizeof(servaddr));
  servaddr.sin_family = AF_INET;
  servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
  servaddr.sin_port = htons(9999);
  if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
      printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
      return 0;
  }
  if (listen(listenfd, 10) == -1) {
      printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
      return 0;
  }
  while (1) {
      struct sockaddr_in client;
      socklen_t len = sizeof(client);
      if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
          printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
          return 0;
      }
      n = recv(connfd, buff, MAXLNE, 0);
      if (n > 0) {
          buff[n] = '\0';
          printf("recv msg from client: %s\n", buff);
    send(connfd, buff, n, 0);
      } else if (n == 0) {
          close(connfd);
      }
2.4、每个连接创建一个线程

这段代码解决了多个连接的问题,但是没办法正常工作。由于accept、recv和send都是阻塞的,程序开始运行到accept等待客户端连接。当一个新的客户端连接后,服务器从Accept全连接队列取出连接,然后新建一个新的客户端fd。 然后程序执行到recv,等待客户端发送消息。服务器收到的消息后将消息回发给客户端,完成一次连接的任务。然后程序再次运行到accept,如果Accept全连接队列不为空,那么会再次从队列中取出一个连接,创建一个新的fd,然后再次执行到recv等待客户端发送消息,以此往复。综上所述,每次只会有一个客户端连接上,并且接收和发送一次消息。

void *client_routine(void *arg) { //
  int connfd = *(int *)arg;
  char buff[MAXLNE];
  while (1) {
    int n = recv(connfd, buff, MAXLNE, 0);
        if (n > 0) {
            buff[n] = '\0';
            printf("recv msg from client: %s\n", buff);
        send(connfd, buff, n, 0);
        } else if (n == 0) {
            close(connfd);
      break;
        }
  }
  return NULL;
}
int main(int argc, char **argv) 
{
    int listenfd, connfd, n;
    struct sockaddr_in servaddr;
    char buff[MAXLNE];
    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(9999);
    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
        printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
    if (listen(listenfd, 10) == -1) {
        printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
    while (1) {
      struct sockaddr_in client;
      socklen_t len = sizeof(client);
      if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
          printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
          return 0;
      }
    pthread_t threadid;
    pthread_create(&threadid, NULL, client_routine, (void*)&connfd);
    }
    close(listenfd);
    return 0;
}

这段代码算不上有问题,在客户端连接数不多的情况下完全是正常的,适用于CPU密集型而非IO密集型的应用场景,比如绘图、运算等场景,但是不适合互联网大量客户端连接的情况。

按照Posix线程分配8M的空间来计算,1G的内存大概只能分配1024M / 8M =128个线程。如果4G的内存最多只能分配512个线程。线程过多会导致内存一直涨,最终导致服务器因为内存不足崩溃重启。


推荐一个零声学院免费公开课程,个人觉得老师讲得不错,分享给大家:Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,点击立即学习:

相关实践学习
通过Ingress进行灰度发布
本场景您将运行一个简单的应用,部署一个新的应用用于新的发布,并通过Ingress能力实现灰度发布。
容器应用与集群管理
欢迎来到《容器应用与集群管理》课程,本课程是“云原生容器Clouder认证“系列中的第二阶段。课程将向您介绍与容器集群相关的概念和技术,这些概念和技术可以帮助您了解阿里云容器服务ACK/ACK Serverless的使用。同时,本课程也会向您介绍可以采取的工具、方法和可操作步骤,以帮助您了解如何基于容器服务ACK Serverless构建和管理企业级应用。 学习完本课程后,您将能够: 掌握容器集群、容器编排的基本概念 掌握Kubernetes的基础概念及核心思想 掌握阿里云容器服务ACK/ACK Serverless概念及使用方法 基于容器服务ACK Serverless搭建和管理企业级网站应用
相关文章
|
3月前
|
网络协议 程序员 API
初识网络编程
本文介绍了网络编程的重要概念,包括IP地址、端口号和协议。IP地址是设备在网络中的唯一标识,IPv4已用尽,IPv6提供了更多地址。端口号用于区分设备上的不同应用程序,取值范围为0~65535。协议定义了网络传输规则,常分为TCP/IP五层模型和OSI七层模型。文章还讨论了TCP与UDP的区别,并提供了UDP协议的简单示例。
51 0
初识网络编程
|
7月前
|
网络协议 API
|
网络协议 关系型数据库 MySQL
网络编程初识
网络编程初识
63 0
|
Java C++
4. 网络编程
4. 网络编程
62 0
|
网络协议
64.【网络编程】(一)
64.【网络编程】
44 0
|
应用服务中间件
64.【网络编程】(三)
64.【网络编程】
44 0
|
网络协议
64.【网络编程】(二)
64.【网络编程】
43 0
|
网络协议 Oracle 安全
网络编程详细讲解
网络编程详细讲解
83 0