网络编程——socket

Socket网络编程

本文字数虽多!但干货满满!简单讲解了TCPSocket的基本框架流程,分别从服务器、客户端两个角度入手,对各函数有基本解析。参考了很多资料、以及大佬们的博客,就不一一列出了!认真读完相信会有很大收获!

概述

 Socket,套接字就是两台主机之间逻辑连接的端点,是网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口

表示方法

S o c k e t = ( I P 地址:端口号) Socket=(IP地址:端口号) Socket=IP地址:端口号)
 套接字的表示方法是点分十进制的lP地址后面写上端口号,中间用冒号或逗号隔开。每一个传输层连接唯一地被通信两端的两个端点(即两个套接字)所确定。假设IP地址是127.0.0.1,而端口号是22,那么得到套接字就是(127.0.0.1:22)。

socket主要类型
  1. 流套接字(SOCK_STREAM)
    流套接字用于提供面向连接、可靠的数据传输服务。该服务将保证数据能够实现无差错、无重复送,并按顺序接收。流套接字之所以能够实现可靠的数据服务,原因在于其使用了传输控制协议,即TCP协议。

  2. 数据报套接字(SOCK_DGRAM)
     数据报套接字提供一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。数据报套接字使用UDP协议进行数据的传输。由于数据报套接字不能保证数据传输的可靠性,对于有可能出现的数据丢失情况,需要在应用层中做相应的处理。

  3. 原始套接字(SOCK_RAW)
     原始套接字与标准套接字(标准套接字指的是前面介绍的流套接字和数据报套接字)的区别在于:原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送的数据必须使用原始套接。

 实际应用中,数据报socket的应用场景比较少,因此以下将介绍流套接字(SOCK_STREAM)。

数据报套接字(SOCK_DGRAM)

在这里插入图片描述

服务端
  1. 创建流式套接字

    int sockfd;
    if ( (sockfd = socket(AF_INET,SOCK_STREAM,0))==-1) {
        perror("socket");
        return -1;
    }
    

     socket函数用于创建一个新的socket,也就是向系统申请一个socket资源,socket函数用户客户端和服务端。

    函数声明:

    int socket(int domain, int type, int protocol);
    
    • domain
       协议域,又称协议族(family)。常用的协议族有AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域Socket)、AF_ROUTE等。协议族决定了socket的地址类型,在通信中必须采用对应的地址 ,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。

    • type
       指定socket类型。常用的socket类型有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等。流式socket(SOCK_STREAM)是一种面向连接的socket,针对于面向连接的TCP服务应用。数据报式socket(SOCK_DGRAM)是一种无连接的socket,对应于无连接的UDP服务应用。

    • protocol
       指定协议。常用协议有IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP、IPPROTO_TIPC等,分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。

     在UNIX系统中,一切输入输出设备皆文件,socket()函数的返回值其本质是一个文件描述符,是一个整数。一般情况下,socket创建都会成功,除非系统资源耗尽,socket函数一般不会返回失败(-1)。

    以下代码可用于测试socket函数:

    #include <iostream>
    #include <netdb.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <arpa/inet.h>
    using namespace std;
    int main()
    {
        int sockfd;
        for (int i = 0; i < 1200; i++)
        {
        	// 创建socket
            if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == 1)
            {
                cout << "socket";
                return -1;
            }
            cout <<  "i = " << i << ",  sockfd = " << sockfd << ";"<< endl;
        }
        return 1;
    }
    

在这里插入图片描述

从i = 1021开始,系统资源耗尽,socket创建失败,返回-1 。也就是说,在本系统中可创建的socket数量为1024个,当然这个数是可调的,在系统配置文件中可修改。

  1. 指定用于通信的IP地址和端口

    struct sockaddr_in servaddr; // 服务端地址信息的数据结构。
    memset(&servaddr, 0, sizeof(servaddr));
    
    // 协议族,在socket编程中只能是AF_INET。
    servaddr.sin_family = AF_INET;
    
    // 服务端程序绑定本主机的任意IP地址 注意 是已经分配给本主机的IP地址中任意取
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); 
    // 指定ip地址
    // servaddr.sin_addr.s_addr = inet_addr("192.168.0.200"); 
    
    // 指定通信端口
    servaddr.sin_port = htons(atoi(argv[1])); 
    
    // 把通信IP、端口与SOCKET进行绑定
    // 注意 此时servaddr是一个结构体,如果要获取其大小只能用sizeof
    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) != 0)
    {
        perror("bind");
        close(listenfd);
        return -1;
    }
    
    • sockaddr结构体
       struct sockaddr 很多网络编程API诞生早于IPv4协议,那时候都使用的是sockaddr结构体。
       IPv4协议普及后,为了向前兼容,sockaddr退化成了(void *)的作用,传递一个地址给函数。至于这个函数是sockaddr_in还是其他的,由地址族确定,然后通过函数内部把地址强制转化为所需的地址类型
      在这里插入图片描述

      #include <netinet/in.h>	//头文件
      
      struct sockaddr		//早期的sockaddr
      {
      	sa_family_t 	sa_family;	/* adress family: AF_XXX */
      	char 			sa_data[14];/* 14 bytes of protocol */
      };
      
      struct sockaddr_in	//IPv4的sockaddr
      {
      	uint8_t 		sin_len;	/* length of structure (16字节) */
      	sa_family_t 	sin_family;	/* AF_INET */
      	in_port_t 		sin_port;	/* 16-bit TCP or UDP port number; 网络字节序  */
      	struct in_addr 	sin_addr;	/* 32-bit IPv4 address; 网络字节序  */
      	char			sin_zero[8];/* unused 把sockaddr_int与sockaddr结构体长度对齐 */
      };
      
      struct in_addr	//IPv4地址
      {
      	in_addr_t 		s_addr;		/* 32-bit IPv4 address; 网络字节序 */
      };
      //由于sock API的实现早于ANSI C标准化,那时还没有void*类型,因此像bind、accept函数
      //的参数都用struct sockaddr* 类型表示, 在传递参数之前要强制转换一下,
      如:
      struct sockaddr_in servaddr;
      bind(listen_fd, (struct sockaddr*)&servaddr, sizeof(servaddr)); 
      
      
    • htonl函数
       将主机的unsigned long值转换成网络字节顺序(32位)(一般主机跟网络上传输的字节顺序是不同的,分大小端),函数返回一个网络字节顺序的数字。

      #include <stdio.h>
      #include <arpa/inet.h>
      int main(int argc, char *argv[])
      {
          // 无符号32位整数
          uint32_t a = 0x1234;
          // %x为16进制显示
          printf("a = 0x%x\n", a);
          printf("a = %d\n", a);
          printf("htonl(a) = 0x%x\n", htonl(a));
          printf("htonl(a) = %d\n ", htonl(a));
          return 0;
      }
      

      结果显示: 在这里插入图片描述 这里要注意,涉及到字节序问题。

       字节序,即字节在电脑中存放时的序列与输入(输出)时的序列是先到的在前还是后到的在前。字节序是指多字节数据在计算机内存中存储或者网络传输时各字节的存储顺序。

       主要有两种字节序

      1. 小端Little endian:将低序字节存储在起始地址

         实际上最符合人的思维的字节序,地址低位存储值的低位,地址高位存储值的高位

         这么讲讲是最符合人的思维的字节序,是因为从人的第一观感来说低位值小,就应该放在内存地址小的地方,也即内存地址低位

         反之,高位值就应该放在内存地址大的地方,也即内存地址高位

      2. 大端Big endian:将高序字节存储在起始地址

         视觉上最直观的字节序,地址低位存储值的高位,地址高位存储值的低位

       在实际应用中,主要有两种字节序,分别是网络字节序和主机字节序:

      1. 主机字节序根据操作系统、硬件等不同,大小端存储方式都会有。
      2. 网络字节序则不同,网络字节序是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。网络字节顺序采用大端big endian(低位放在高字节,高位放在低字节)排序方式
    • bind函数,服务端把用于通信的地址和端口绑定到socket上。
       函数声明:

      int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
      
      • 参数sockfd,需要绑定的socket,是一个文件描述符。

      • 参数addr,存放了服务端用于通信的地址和端口,用结构体sockaddr_in强制转换得到。

      • 参数addrlen,表示addr结构体的大小。

      • 返回值:成功则返回0,失败返回-1,错误原因存于errno 中。

    如果绑定的地址错误,或端口已被占用,bind函数一定会报错,否则一般不会返回错误。

  2. 把SOCKET设置为监听模式

    if (listen(listenfd, 5) != 0)
    {
        perror("listen");
        close(listenfd);
        return -1;
    }
    

     listen函数把主动连接socket变为被动连接的socket,使得这个socket可以接受其它socket的连接请求,从而成为一个服务端的socket。
     函数声明:

    //返回:0-成功, -1-失败 	listen函数一般不会返回错误。
    int listen(int sockfd, int backlog);
    
    • 参数sockfd,已经被bind过的socket。
       socket函数返回的socket是一个主动连接的socket,在服务端的编程中,程序员希望这个socket可以接受外来的连接请求,也就是被动等待客户端来连接。需要通过某种方式来告诉系统当前的socket是可连接的,则可以通过调用listen函数来完成这件事。

    • 参数backlog,这个参数涉及请求队列的大小。
       当套接字正在处理客户端请求时,如果有新的请求进来,套接字是没办法处理的,只能把请求放进缓冲区,等待当前请求结束后,再从缓冲区中读取出来进行处理。如果不断有新的请求进来,按照先后顺序在缓冲区排队,直到缓冲区满,这个缓冲区就称为请求队列。
      请求队列也称为全连接队列,指的是该队列中的请求已经完成了TCP的三次握手连接,但如果该队列满了后,请求应该怎么处理?此时引出了一个半连接对队列。
       我们知道,TCP三次握手连接中客户端首先发出SYH请求,服务端收到后处于REC_SYN状态,然后发送ACK+SYN传给客户端,客户端收到后再最后发送ACK给服务端即可完成三次握手建立TCP连接。
      当全连接队列满后,请求会放到半连接队列中,此时连接请求并不会马上向客户端回复一个ACK+SYN报文,而是等待全连接队列的位置。当全连接队列有空位置,就可以把请求从从半连接队列中拉出来,同时发送ACK+SYN报文给客户端建立TCP连接,如果收到ACK才把请求真正的放到全连接队列中等待处理。
       当半连接队列满了后,请求怎么处理?这时候有两个选择,直接丢弃请求或者向客户端发送连接失败,这部分需要配置一下系统文件,默认是丢弃请求。

    当调用listen之后,服务端的socket就可以调用accept来接受客户端的连接请求。

  3. 接收客户端连接

    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);
    printf("客户端(%s)已连接。\n", inet_ntoa(clientaddr.sin_addr));
    

    函数声明:

    // 返回值:成功则返回0,失败返回-1,错误原因存于errno 中。
    // accept在等待的过程中,如果被中断或其它的原因,函数返回-1,表示失败,如果失败,可以重新accept。
    int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
    
    • 参数sockfd是已经被listen过的socket。
    • 参数addr用于存放客户端的地址信息,用sockaddr结构体表达,如果不需要客户端的地址,可以填0。
    • 参数addrlen用于存放addr参数的长度,如果addr为0,addrlen也填0。

    accept函数等待客户端的连接,如果没有客户端连上来,它就一直等待,这种方式称之为阻塞。

     accept等待到客户端的连接后,创建一个新的socket,函数返回值就是这个新的socket,服务端使用这个新的socket和客户端进行报文的收发

     accept在等待的过程中,如果被中断或其它的原因,函数返回-1,表示失败,如果失败,可以重新accept。

  4. 接收/发送数据

    char buffer[1024];
    while (1)
    {
        int iret;
        memset(buffer, 0, sizeof(buffer));
        if ((iret = recv(clientfd, buffer, sizeof(buffer), 0)) <= 0)
        { // 接收客户端的请求报文。
            printf("iret=%d\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);
    }
    

    send函数用于把数据通过socket发送给对端。不论是客户端还是服务端,应用程序都用send函数来向TCP连接的另一端发送数据。

    函数声明:

    // 函数返回已发送的字符数。出错时返回-1,错误信息errno被标记
    // 如果send函数返回的错误(<=0),表示通信链路已不可用。
    ssize_t send(int sockfd, const void *buf, size_t len, int flags);
    
    • sockfd:已建立好连接的socket。
    • buf:需要发送的数据的内存地址,可以是C语言基本数据类型变量的地址,也可以数组、结构体、字符串,内存中有什么就发送什么。
    • len:需要发送的数据的长度,为buf中有效数据的长度。
    • flags填0, 其他数值意义不大。

 注意,就算是网络断开,或socket已被对端关闭,send函数不会立即报错,要过几秒才会报错。
recv函数用于接收对端socket发送过来的数据。
 recv函数用于接收对端通过socket发送过来的数据。不论是客户端还是服务端,应用程序都用recv函数接收来自TCP连接的另一端发送过来数据。
 函数声明:

// 函数返回已接收的字符数。出错时返回-1,失败时不会设置errno的值。
// 如果recv函数返回的错误(<=0),表示通信通道已不可用。
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
  • sockfd:已建立好连接的socket。
  • buf:用于接收数据的内存地址,可以是C语言基本数据类型变量的地址,也可以数组、结构体、字符串,只要是一块内存就行了。
  • len:需要接收数据的长度,不能超过buf的大小,否则内存溢出。
  • flags填0, 其他数值意义不大。

 如果socket的对端没有发送数据,recv函数就会等待,如果对端发送了数据,函数返回接收到的字符数出错时返回-1。如果socket被对端关闭,返回值为0

 send函数和recv函数和其实都是类似的,不再说明。再有write()函数和send函数功能相似,read()函数和recv()函数也是相似的。

  1. 关闭SOCKET连接,释放资源

    close(listenfd);
    close(clientfd);
    

    退出时一定要关闭socket通道,与指针类似,指针在后续代码无需用到时需要delete

 完整代码:

#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>

// 创建服务端socket->绑定端口、地址->设置监听模式(被动模式) ->等待连接->连接->通信->关闭socket释放资源
int main(int argc, char *argv[])
{
	if (argc != 2)
	{
		printf("Using:./server port\nExample:./server 5005\n\n");
		return -1;
	}

	// 第1步:创建服务端的socket。
	int listenfd;
	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地址 注意 是已经分配给本主机的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)
	{
		perror("listen");
		close(listenfd);
		return -1;
	}

	// 第4步:接受客户端的连接。
	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);
	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\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);
}

需要注意,该代码应该在Linux平台上运行,如果在window平台运行编译器可能需要添加额外的库。

客户端
  1. 创建流式套接字

    if (argc != 3)
    {
        printf("Using:./client ip port\nExample:./client 127.0.0.1 5005\n\n");
        return -1;
    }
    
  2. 向服务器发起连接请求

    struct hostent *h;
    if ((h = gethostbyname(argv[1])) == 0)
    { // 指定服务端的ip地址。
        printf("gethostbyname failed.\n");
        close(sockfd);
        return -1;
    }
    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(atoi(argv[2])); // 指定服务端的通信端口。
    memcpy(&servaddr.sin_addr, h->h_addr, h->h_length);
    if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) != 0)
    { // 向服务端发起连接清求。
        perror("connect");
        close(sockfd);
        return -1;
    }
    

    gethostbyname函数ip地址或域名转换为hostent 结构体表达的地址。如果直接给sin_addr赋值,则不能实现域名分析。

    函数声明:

    struct hostent *gethostbyname(const char *name);
    
    • 参数name,域名或者主机名,例如"192.168.0.1"、"www.baidu.com"等。
    • 返回值:如果成功,返回一个hostent结构指针,失败返回NULL。

    gethostbyname只用于客户端。gethostbyname只是把字符串的ip地址转换为结构体的ip地址,只要地址格式没错,一般不会返回错误,失败时不会设置errno的值。

    connect函数向服务器发起连接请求。

 函数声明:

int connect(int sockfd, struct sockaddr * serv_addr, int addrlen);

 connect函数用于将参数sockfd 的socket 连至参数serv_addr 指定的服务端,参数addrlen为sockaddr的结构长度。

  • 返回值:成功则返回0,失败返回-1,错误原因存于errno 中。

connect函数只用于客户端,,如果服务端的地址错了,或端口错了,或服务端没有启动,connect一定会失败。

  1. 发送/接收数据

    for (int ii = 0; ii < 3; ii++)
    {
        int iret;
        memset(buffer, 0, sizeof(buffer));
        sprintf(buffer, "这是第%d个请求报文,编号%03d。", ii + 1, ii + 1);
        if ((iret = send(sockfd, buffer, strlen(buffer), 0)) <= 0)
        { // 向服务端发送请求报文。
            perror("send");
            break;
        }
        printf("发送:%s\n", buffer);
    
        memset(buffer, 0, sizeof(buffer));
        if ((iret = recv(sockfd, buffer, sizeof(buffer), 0)) <= 0)
        { // 接收服务端的回应报文。
            printf("iret=%d\n", iret);
            break;
        }
        printf("接收:%s\n", buffer);
    }
    
  2. 断开SOCKET连接

    close(sockfd)
    

 完整代码:

#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 != 3)
	{
		printf("Using:./client ip port\nExample:./client 127.0.0.1 5005\n\n");
		return -1;
	}

	// 第1步:创建客户端的socket。
	int sockfd;
	if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
	{
		perror("socket");
		return -1;
	}

	// 第2步:向服务器发起连接请求。
	struct hostent *h;
	if ((h = gethostbyname(argv[1])) == 0)
	{ // 指定服务端的ip地址。
		printf("gethostbyname failed.\n");
		close(sockfd);
		return -1;
	}
	struct sockaddr_in servaddr;
	memset(&servaddr, 0, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(atoi(argv[2])); // 指定服务端的通信端口。
	memcpy(&servaddr.sin_addr, h->h_addr, h->h_length);
	if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) != 0)
	{ // 向服务端发起连接清求。
		perror("connect");
		close(sockfd);
		return -1;
	}

	char buffer[1024];

	// 第3步:与服务端通信,发送一个报文后等待回复,然后再发下一个报文。
	for (int ii = 0; ii < 3; ii++)
	{
		int iret;
		memset(buffer, 0, sizeof(buffer));
		sprintf(buffer, "这是第%d个请求报文,编号%03d。", ii + 1, ii + 1);
		if ((iret = send(sockfd, buffer, strlen(buffer), 0)) <= 0)
		{ // 向服务端发送请求报文。
			perror("send");
			break;
		}
		printf("发送:%s\n", buffer);

		memset(buffer, 0, sizeof(buffer));
		if ((iret = recv(sockfd, buffer, sizeof(buffer), 0)) <= 0)
		{ // 接收服务端的回应报文。
			printf("iret=%d\n", iret);
			break;
		}
		printf("接收:%s\n", buffer);
	}

	// 第4步:关闭socket,释放资源。
	close(sockfd);
}
测试结果

服务器端
在这里插入图片描述

 打开5050端口,等待client连接

客户端
在这里插入图片描述

 运行客户端,传入IP以及端口,向服务器发送请求
在这里插入图片描述

拓展

socket缓冲区

 每个socket被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。

 write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情

 TCP协议独立于 write()/send() 函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。

 read()/recv() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取。

在这里插入图片描述
总结如下:

  • IO缓冲区在每个套接字缓冲区中单独存在
  • IO缓冲区在创建套接字时自动生成
  • 关闭套接字也不会影响缓冲区的数据传输

 输入输出缓冲区的默认大小一般都是 8K,可以通过 getsockopt() 函数获取。
 对于TCP套接字(默认情况下),当使用 write()/send() 发送数据时:

  • 首先会检查缓冲区,如果缓冲区的可用空间长度小于要发送的数据,那么 write()/send() 会被阻塞(暂停执行),直到缓冲区中的数据被发送到目标机器,腾出足够的空间,才唤醒 write()/send() 函数继续写入数据
  • 如果**TCP协议正在向网络发送数据,那么输出缓冲区会被锁定,不允许写入,**write()/send() 也会被阻塞,直到数据发送完毕缓冲区解锁,write()/send() 才会被唤醒。
  • 如果要写入的数据大于缓冲区的最大长度,那么将分批写入
  • 直到所有数据被写入缓冲区 write()/send() 才能返回。

 当使用 read()/recv() 读取数据时:

  • 首先会检查缓冲区,如果缓冲区中有数据,那么就读取,否则函数会被阻塞,直到网络上有数据到来。
  • 如果要读取的数据长度小于缓冲区中的数据长度,那么就不能一次性将缓冲区中的所有数据读出,剩余数据将不断积压,直到有 read()/recv() 函数再次读取。
  • 直到读取到数据后 read()/recv() 函数才会返回,否则就一直被阻塞。
     这就是TCP套接字的阻塞模式。所谓阻塞,就是上一步动作没有完成,下一步动作将暂停,直到上一步动作完成后才能继续,以保持同步性。
粘包拆包的问题

 TCP是面向字节流的协议,就是没有界限的一串数据,本没有“包”的概念,“粘包”和“拆包”一说是为了有助于形象地理解这两种现象。

 为什么UDP没有粘包?因为粘包问题在数据链路层、网络层以及传输层都有可能发生。日常的网络应用开发大都在传输层进行,由于UDP有消息保护边界,不会发生粘包拆包问题(UDP是面向报文协议),因此粘包拆包问题只发生在TCP协议中。

 粘包拆包发生场景:

 因为TCP是面向流,没有边界,而操作系统在发送TCP数据时,会通过缓冲区来进行优化,例如缓冲区为1024个字节大小。

如果一次请求发送的数据量比较小,没达到缓冲区大小,TCP则会将多个请求合并为同一个请求进行发送,这就形成了粘包问题

如果一次请求发送的数据量比较大,超过了缓冲区大小,TCP就会将其拆分为多次发送,这就是拆包。在数据链路层、网络层以及传输层都有可能发生。日常的网络应用开发大都在传输层进行,由于UDP有消息保护边界,不会发生粘包拆包问题,因此粘包拆包问题只发生在TCP协议中。

 粘包拆包问题在缓冲区内得以体现,假设现服务器正在处理请求被阻塞,此时又有不同的数据涌进来,只能把数据堆放在缓冲区内,此时由于不存在边界。则多个数据包”粘“在了一起;当服务器处理完请求后,需要在缓冲区内取出数据,分别处理,此时就需要拆包,通过recv函数中限定每次读取缓冲区的长度便可以实现拆包。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值