被用户独占的服务器(1983年 socket发布在Unix4.2 BSD 没有I/O复用)

本文详细介绍了如何使用C语言实现一个简单的TCP服务器,包括创建socket、绑定地址、监听连接、接收和发送数据等步骤。服务器在接收到客户端请求后,回复'ok',并在客户端断开连接后能继续接受新的连接。然而,由于recv和accept的阻塞特性,该服务器只能一对一处理客户端请求,资源利用率不高。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>

int main(int argc, char *argv[])
{
  if (argc != 2)
  {
    printf("Using:./server port\nExample:./server 5005\n\n");
    return -1;
  }
  // 第1步:创建服务端的socket,和文件描述符一样
  int listenfd;
  // 初始化时只指定了所用的底层协议族为AF_INET(本地域通信),传输层使用SOCK_STREAM(字节流协议),即TCP协议
  if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) 
  {
    perror("socket");
    return -1;
  }
  // 第2步:把服务端用于通信的地址和端口绑定到socket上。
  struct sockaddr_in servaddr; // 服务端地址信息的数据结构。
  memset(&servaddr, 0, sizeof(servaddr));
  servaddr.sin_family = AF_INET;                // 协议族,在socket编程中只能是AF_INET。
  servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 任意ip地址。
  // servaddr.sin_addr.s_addr = inet_addr("192.168.190.134"); // 指定ip地址。
  servaddr.sin_port = htons(atoi(argv[1])); // 指定通信端口。
  if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) != 0)
  {
    perror("bind");
    close(listenfd);
    return -1;
  }
  // 第3步:把socket设置为监听模式。
  if (listen(listenfd, 5) != 0) // listen函数创建一个listen监听队列用于存放用户连接!!!
  {
    perror("listen");
    close(listenfd);
    return -1;
  }
  printf("Fininsh listening, try to accept...\n");
  // 第4步:接受客户端的连接。
  int clientfd;                             // 客户端的socket。
  int socklen = sizeof(struct sockaddr_in); // struct sockaddr_in的大小
  struct sockaddr_in clientaddr;            // 客户端的地址信息。
  // accept从listen监听队列中取出一个用户连接,当监听队列为空时,accept陷入阻塞!!!
  clientfd = accept(listenfd, (struct sockaddr *)&clientaddr, (socklen_t *)&socklen);
  printf("客户端(%s)已连接。\n", inet_ntoa(clientaddr.sin_addr));

  // 第5步:与客户端通信,接收客户端发过来的报文后,回复ok。
  char buffer[1024];
  while (1)
  {
    int iret;
    memset(buffer, 0, sizeof(buffer));
    if ((iret = recv(clientfd, buffer, sizeof(buffer), 0)) <= 0) // 接收客户端的请求报文。
    {
      printf("iret=%d, waitting next connection\n", iret);
      clientfd = accept(listenfd, (struct sockaddr *)&clientaddr, (socklen_t *)&socklen);
      printf("客户端(%s)已连接。\n", inet_ntoa(clientaddr.sin_addr));
      // break;
    }
    printf("接收:%s\n", buffer);

    strcpy(buffer, "ok");
    if ((iret = send(clientfd, buffer, strlen(buffer), 0)) <= 0) // 向客户端发送响应结果。
    {
      perror("send");
      break;
    }
    printf("发送:%s\n", buffer);
  }

  // 第6步:关闭socket,释放资源。
  close(listenfd);
  close(clientfd);
}

重点关注while循环体,在这个循环体中recv和send都是阻塞式的函数,如果用户没有向服务器发送数据,则循环将阻塞在recv函数中,整个服务器也进入了阻塞,不能做任何事情。所以这种实现方式中服务器和客户端的连接是一对一的。

如果当前的用户断开连接,则recv函数返回-1,if判断生效,使用accept函数接收下一个用户的连接请求。注意,accept函数也是阻塞的,如果没有下一个用户请求,服务器将在这个位置阻塞。

这个的服务器程序也可以处理多个用户的请求,但必须得当前用户结束之后才能连接下一个用户。这就像是一个用户独占了整个服务器,这个用户下线后下一个用户接着独占这个服务器。所以服务器的资源是远远没有充分利用起来的,绝大部分时间可能都阻塞在recv和accept两个函数。

补充一点,当进程进入阻塞态时,它会被操作系统加入到阻塞队列,同时操作系统从就绪队列中选择新的进程交给cpu处理。所以阻塞的队列是不占用cpu的。但是在挂起时,这个阻塞的进程所拥有的一些资源会被释放掉,当它被再次唤醒时,需要重新分配资源,从阻塞态变为就绪态称为中程调度,这个切换代价较高,会降低程序的执行效率。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值