Linux C网络通信过程

socket函数、sockaddr_in结构体 和 bind函数

socket函数的作用是创建一个网络文件描述符,程序通过这个文件描述符将数据发送到网络,也通过这个文件描述符从网络中接受数据。观察一下socket函数:

int listenfd;
listenfd = socket(AF_INET, SOCK_STREAM, 0)

会发现这个函数有三个参数,其中前两个参数指定了底层协议族为AF_INET(进行本地域通信),传输层使用SOCK_STREAM(字节流协议),即TCP协议。

我们知道,在传输层中网络是通过套接字(ip,端口)来进行定位的,但是socket中并没有指定套接字。默认情况下,系统会会随意分配一个端口,使用本机的ip地址。

这在客户端是没有问题的,客户端可以选择任意的端口和服务器进行通信。但是,服务器不行,因为客户端是主动向服务器发送数据的,它需要知道数据应该发送到服务器的哪个端口。所以需要服务器事先指定好端口号,服务器通过这个端口向客户端发送数据,也通过这个端口接收客户端发来的数据。

由此,需要一个套接字结构体sockaddr_in来定义套接字:

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_port = htons(atoi(argv[1])); // 指定通信端口。

在这个套接字中,除了ip和端口,也指定了协议族,该协议族应该和传递给socket函数的一致。这里INADDR_ANY表示本机的任意ip地址,因为有些服务器不止一块网卡,多网卡的情况下,用该参数表示所有网卡的ip地址。ip地址也可以通过servaddr.sin_port = htons(atoi(argv[1]))的方式指定特定ip地址。

好了,现在有了套接字,但是和先前定义的socket文件描述符没有关系,所以还需要通过bind函数进行绑定:

bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

这个过程也叫socket命名。

上面的过程配置的是服务器的套接字,对于服务器来说,这是数据发送的起点,要将数据发送给客户端,还需要知道数据的终点,也就是客户端的套接字。客户端的套接字可以用accept函数获取。

listen监听队列、accept函数 及 数据的收发

当服务器得到socket文件描述符后,就可以准备和用户进行通信了。由于一般有多个客户端,所以服务器会通过一个监听队列来保存用户的连接请求。该监听队列通过listen函数来创建:

listen(listenfd, 5);

这里listen的是服务器的listenfd套接字,对客户端来说,这是数据的终点,所以服务器可以针对这个套接字进行监听。当有数据发送到这个套接字的时候,服务器将这个连接请求放入到listen监听队列。

当服务器要处理客户端连接请求时,通过accept函数选择一个连接。客户端发送的请求到达服务器后,服务器可以从报文的报头部分获得客户端的ip地址和端口号。
创建一个客户端套接字,服务器将这些信息保存到该套接字中,以便后续通信中将数据发送到客户端套接字。

int clientfd;                             // 客户端的socket。
int socklen = sizeof(struct sockaddr_in); // struct sockaddr_in的大小
struct sockaddr_in clientaddr;            // 客户端的地址信息。
clientfd = accept(listenfd, (struct sockaddr *)&clientaddr, (socklen_t *)&socklen);

需要注意的是,accept是阻塞式的,当监听队列中没有请求连接时,accept将进入阻塞状态。

这就意味着,在传统的网络通信模型中,单线程的服务器将只能处理一个连接请求,只有当前的请求完成后,才能处理下一个请求,这是串行的处理方式。

梳理一下:服务器在处理客户端请求的过程中,将涉及到两个套接字,服务器通过监听本地的套接字来获取连接请求;之后通过accept获取客户端套接字,进行数据的收发,所以在进行数据收发时,只需要用到客户端套接字clientfd

char buffer[1024];
memset(buffer, 0, sizeof(buffer));
recv(clientfd, buffer, sizeof(buffer), 0);	\\接收数据
strcpy(buffer, "ok");
send(clientfd, buffer, strlen(buffer), 0);	\\发送数据

连接的释放

当客户端主动释放连接后,客户端和服务器之间将进行四次挥手,其中第四次挥手可以通过netstat -nt | grep port观察到

比如下面的服务器程序,该程序的功能是接受客户端发送的信息,再将信息发送回去。

/*
 * 程序名:server.cpp,此程序用于演示socket通信的服务端
 * 作者:C语言技术网(www.freecplus.net) 日期:20190525
 */
#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);
      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);
}

首先通过gcc -o server ./server.c编译得到可执行文件,再通过./server 1234启动该服务程序后,可以通过telnet 127.0.0.1 1234在本地建立和服务器(本机)的连接。

随后,通过netstat -nt | grep 1234可以查看连接释放后端口的使用情况,如下:

tcp        0      0 127.0.0.1:1234          127.0.0.1:60026         TIME_WAIT  

和1234有关的连接只有一个,这是客户端到服务器的连接,该连接处于TIME_WAIT状态。对照四次挥手示意图,可以看到客户端的这个连接将会持续2MSL。此时再次通过1234端口来启动服务器将会出现端口占用的提示:

bind: Address already in use

在这里插入图片描述
简单分析一下,当客户端接受到FIN=1,ACK=1后,客户端知道服务器要释放连接了,于是发送报文ACK=1,在收到该报文之前服务器处于LAST_ACK状态,并未释放连接资源。

如果网络不好,服务器可能收不到该报文,一直处于LAST_ACK状态,并且把原因归咎为客户端没有收到FIN=1, ACK=1报文,所以服务器会继续发送该报文。此时客户端等待的时间没有超过2MSL并处于TIME_WAIT状态,接受到该报文后他明白服务器的连接还未释放,所以再次发送ACK=1报文,通知服务器正常释放连接,避免服务器资源的消耗

网络正常的情况下,当2MSL过去后,第四次挥手顺利完成,再次使用netstat -nt | grep 1234查看会发现该连接自动释放掉了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值