网络数据到底怎样的传输过程?什么是网络编程?一文教你清晰入门linux下socket网络编程—— 服务端篇(TCP协议传输)!

socket网络编程入门篇之服务端篇

  • socket网络编程入门篇之服务端篇
    • 1、网络编程入门篇——前章
      • 1.1、网络数据传输过程
      • 1.2、什么是socket套接字
      • 1.3、网络结构体
      • 1.4、网络字节序 (Network Byte Order)和本机转换
    • 2、网络编程入门篇—— 服务端篇
      • 2.1、创建socket()
      • 2.2、命名(又叫绑定)bind()
        • 2.2.1、TIME_WAIT状态引起的bind失败的方法?
      • 2.3、监听socket()(listen())
      • 2.4、接受连接(accept(),重要)
        • 2.4.1、connect()、listen()和accept()三者之间的关系
      • 2.5、数据读写(send()+recv())
        • 2.5.1、数据读(recv())
        • 2.5.2、数据写(send())
      • 2.6、关闭连接 (close() and shutdown())
        • 2.6.1、close()
        • 2.6.2、shutdown()
        • 2.6.3、shutdown()和close()区别
    • 3、网络编程入门篇——客户端篇
      • 2.1、创建socket()
      • 2.2、发起连接(connect(),面试常问)
      • 2.3、数据读写(send()+recv())
        • 2.3.1、数据读(recv())
        • 2.3.2、数据写(send())
      • 2.4、关闭连接 (close() and shutdown())
        • 2.4.1、close()
        • 2.4.2、shutdown()
        • 2.4.3、shutdown()和close()区别
    • 4、linux socket实现简单的服务器和客户端对话
  • 参考

socket网络编程入门篇之服务端篇

1、网络编程入门篇——前章

1.1、网络数据传输过程

总体流程如下图server(服务器) ——》 client(客户)
在这里插入图片描述
下面会细致展开

1、网卡接收数据
计算机由CPU、存储器(内存)、网络接口等部件组成。
在这里插入图片描述
下图展示了网卡接收数据的过程。

  • 在①阶段,网卡收到网线传来的数据;
  • 经过②阶段的硬件电路的传输;
  • 最终将数据写入到内存中的某个地址上(③阶段)。

这个过程涉及到DMA传输、IO通路选择等硬件有关的知识,但我们只需知道:网卡会把接收到的数据写入内存。

在这里插入图片描述
2、内核接收网络数据

  • 1、计算机收到了对端传送的数据(步骤①);

  • 2、数据经由网卡传送到内存(步骤②);

  • 3、然后网卡通过中断信号通知cpu有数据到达,cpu执行中断程序(步骤③)。此处的中断程序主要有两项功能;

    • 先将网络数据写入到对应socket的接收缓冲区里面(步骤④);
    • 再唤醒进程A(步骤⑤),重新将进程A放入工作队列中。

在这里插入图片描述

注:
蓝色区域里面的等待队列:就是用户空间进程调用recv函数(读取数据)请求读取内核缓冲区内的数据,由于缓冲区数据没有准备好,所以处于等待状态(又称为阻塞状态)。(这里看不懂就不用深究啦)

你有这样的疑问吗?操作系统如何知道网络数据对应于哪个socket套接字

因为一个socket对应着一个端口号,而网络数据包中包含了ip和端口的信息,内核可以通过端口号找到对应的socket。当然,为了提高处理速度,操作系统会维护端口号到socket的索引结构,以快速读取。

1.2、什么是socket套接字

在这里插入图片描述

你也许听到一些Unix高手(hacker)这样说过:“呀,Unix中的一切就是文件!”

那个家伙也许正在说到一个事实:Unix 程序在执行任何形式的 I/O 的时候,程序是在读或者写一个文件描述符。

  • 一个文件描述符只是一个和打开的文件相关联的整数;
  • 这个文件可能是一个网络连接,FIFO,管道,终端,磁盘上的文件或者什么其它的东西。

Socket就像一个电话插座,负责连通两端的电话,进行点对点通信,让电话可以进行通信,端口就像插座上的孔,端口不能同时被其他进程占用。而我们建立连接就像把插头插在这个插座上,创建一个Socket实例开始监听后,这个电话插座就时刻监听着消息的传入,谁拨通我这个“IP地址和端口”,我就接通谁。

socket 和 文件描述符之间的关系?

套接字也是文件。具体数据传输流程如下:

  • server端监听到有连接时,应用程序会请求内核创建Socket

  • Socket创建好后会返回一个文件描述符给应用程序;

  • 当有数据包过来网卡时,内核会通过数据包的源端口,源ip,目的端口等在内核维护的一个ipcb双向链表中找到对应的Socket,并将数据包赋值到该Socket的缓冲区

  • 应用程序请求读取Socket中的数据时,内核就会将数据拷贝到应用程序的内存空间,从而完成读取Socket数据

注意:

  • 1、操作系统针对不同的传输方式(TCP,UDP)会在内核中各自维护一个Socket双向链表,当数据包到达网卡时,会根据数据包的源端口,源ip,目的端口从对应的链表中找到其对应的Socket,并会将数据拷贝到Socket的缓冲区,等待应用程序读取。(上面有图)
  • 2、socket跟TCP/IP并没有必然的联系。Socket编程接口在设计的时候,就希望也能适应其他的网络协议。所以,socket的出现只是可以更方便的使用TCP/IP协议栈而已,其对TCP/IP进行了抽象,形成了几个最基本的函数接口。比如create,listen,accept,connect,read和write等等。

想了解文件描述符(fd)是什么请参考此链接:
文件描述符(file descriptor)是什么?socket 和 文件描述符之间的关系?

总结:

所以,你想和Internet上别的程序通讯的时候,你将要使用到文件描述符。你必须理解刚才的话。现在你脑海中或许冒出这样的念头:“那么我从哪里得到网络通讯的文件描述符呢?”,这个问题无论如何我都要回答:你利用系统调用 socket()它返回套接字描述符 (socket descriptor)然后你再通过它来进行send() 和 recv()调用。

1.3、网络结构体

struct sockaddr
这个结构 为许多类型的套接字储存套接字地址信息:

struct sockaddr {
  sa_family_t sa_family; /* 地址家族, AF_xxx */
  char sa_data[14]; /*14字节协议地址*/
};

sa_family成员是地址族类型( sa_family_t)的变量。地址族类型通常与协议族类型对应。常见的协议族( protocol family,也称 domain,如下表

在这里插入图片描述
sa_data成员用于存放 socket地址值。但是,不同的协议族的地址值具有不同的含义和长度,如下表所示。
在这里插入图片描述
由表5-2可见,14字节的 sa_data根本无法完全容纳多数协议族的地址值。

为了处理struct sockaddr,程序员创造了一个并列的结构: struct sockaddr_in (“in” 代表 “Internet”。)

struct sockaddr_in:

#include 

struct sockaddr_in {

  short int sin_family; /* 通信类型 */
  unsigned short int sin_port; /* 端口 */
  struct in_addr sin_addr; /* Internet 地址 */
  unsigned char sin_zero[8]; /* 与sockaddr结构的长度相同*/
};

//port和addr 分开储存在两个变量中


/* Internet 地址 (存放32 位IP地址) */

struct in_addr {

  unsigned long s_addr;

};

所以:

  • 一般先把sockaddr_in变量赋值后,强制类型转换后传入用sockaddr做参数的函数:
  • sockaddr_in用于socket定义和赋值;sockaddr用于函数参数。

网络数据到底怎样的传输过程?什么是网络编程?一文教你清晰入门linux下socket网络编程—— 服务端篇(TCP协议传输)!_第1张图片

实例

#include 
#include 
#include 
#include 

int main(int argc,char **argv)
{
    int sockfd;
    struct sockaddr_in mysock;

    sockfd = socket(AF_INET,SOCK_STREAM,0);  //获得fd

    bzero(&mysock,sizeof(mysock));  //初始化结构体
    mysock.sin_family = AF_INET;  //设置地址家族
    mysock.sin_port = htons(800);  //设置端口
    mysock.sin_addr.s_addr = inet_addr("192.168.1.0");  //设置地址
    bind(sockfd,(struct sockaddr *)&mysock,sizeof(struct sockaddr); /* bind的时候进行转化 */
    ... ...
    return 0;
}

1.4、网络字节序 (Network Byte Order)和本机转换

1、大端、小端字节序
“大端”和”小端”表示多字节值的哪一端存储在该值的起始地址处;小端存储在起始地址处,即是小端字节序;大端存储在起始地址处,即是大端字节序;具体的说:

  • ①大端字节序(Big Endian):最高有效位存于最低内存地址处,最低有效位存于最高内存处;
  • ②小端字节序(Little Endian):最高有效位存于最高内存地址,最低有效位存于最低内存处。

如下图:当以不同的存储方式,存储数据为0x12345678时:
在这里插入图片描述
网络字节序:大端字节序
网络上传输的数据都是字节流,对于一个多字节数值,在进行网络传输的时候,先传递哪个字节?也就是说,当接收端收到第一个字节的时候,它将这个字节作为高位字节还是低位字节处理,是一个比较有意义的问题:

UDP/TCP/IP协议规定:把接收到的第一个字节当作高位字节看待,这就要求发送端发送的第一个字节是高位字节;而在发送端发送数据时,发送的第一个字节是该数值在内存中的起始地址处对应的那个字节,也就是说,该数值在内存中的起始地址处对应的那个字节就是要发送的第一个高位字节

所以:网络字节序就是大端字节序, 有些系统的本机字节序是小端字节序, 有些则是大端字节序, 为了保证传送顺序的一致性, 所以网际协议使用大端字节序来传送数据

字节序转换函数

 #include 

//将主机字节序转换为网络字节序
 unit32_t htonl (unit32_t hostlong);
 unit16_t htons (unit16_t hostshort);
 //将网络字节序转换为主机字节序
 unit32_t ntohl (unit32_t netlong);
 unit16_t ntohs (unit16_t netshort);

 说明:h -----host;n----network ;s------short;l----longhtons()--"Host to Network Short"
htonl()--"Host to Network Long"
ntohs()--"Network to Host Short"
ntohl()--"Network to Host Long"

为什么在数据结构 struct sockaddr_in 中, sin_addr 和 sin_port 需要转换为网络字节顺序,而sin_family 需不需要呢?

答案是: sin_addrsin_port 分别封装在包的 IPUDP 层。因此,它们必须要 是网络字节顺序。但是 sin_family 域只是被内核 (kernel) 使用来决定在数 据结构中包含什么类型的地址,所以它必须是本机字节顺序。同时, sin_family 没有发送到网络上,它们可以是本机字节顺序。

IP 地址如何处理:地址转换函数

IP地址的三种表示格式及在开发中的应用

  • 1)点分十进制表示格式

  • 2)网络字节序格式

  • 3)主机字节序格式

用IP地址127.0.0.1为例:

   第一步   127   .     0     .     0      .    1   把IP地址每一部分转换为8位的二进制数。

  第二步 01111111     00000000     00000000     00000001      =   2130706433   (主机字节序)

  然后把上面的四部分二进制数从右往左按部分重新排列,那就变为:

  第三步 00000001     00000000     00000000    01111111        =   16777343        (网络字节序)

1、函数inet_addr(),将IP地址从 点数格式转换成无符号长整型。使用方法如下:

函数原型

in_addr_t inet_addr(const char *cp);

转换网络主机地址(点分十进制)为网络字节序二进制值,

  • cp代表点分十进制的IP地址,如1.2.3.4
  • 如果参数 char *cp 无效则返回-1(INADDR_NONE),
  • 但这个函数有个缺点:在处理地址为255.255.255.255时也返回-1,虽然它是一个有效地址,但inet_addr()无法处理这个地址。

使用

ina.sin_addr.s_addr = inet_addr("132.241.5.10");

现在你可以将IP地址转换成长整型了。有没有其相反的方法呢? 它可以将一个in_addr结构体输出成点数格式?

2、你就要用到函数 inet_ntoa()(“ntoa"的含义是"network to ascii”),就像这样:
函数原型

char* inet_ntoa(struct in_addr in);

参数:

  • in代码in_addr的结构体,其结构体如下:
struct in_addr 
{
    union 
    {
        struct { UCHAR s_b1,s_b2,s_b3,s_b4; } S_un_b;
        struct { USHORT s_w1,s_w2; } S_un_w;
        ULONG S_addr;
    } S_un;
};

使用

SOCKADDR_IN sock;
sock.sin_family = AF_INET;
//将字符串转换为in_addr类型
sock.sin_addr.S_un.S_addr =  inet_addr("192.168.1.111");
sock.sin_port = htons(5000);
 
//将in_addr类型转换为字符串
printf("inet_ntoa ip = %s\n",inet_ntoa(sock.sin_addr));


结果输出:
inet_ntoa ip = 192.168.1.111

注意:
inet_ntoa()将结构体in_addr作为一个参数,不是长整形。同样需要注意的是它返回的是一个指向一个字符的 指针。它是一个由inet_ntoa()控制的静态的固定的指针,所以每次调用 inet_ntoa(),它就将覆盖上次调用时所得的IP地址。例如:

char *a1, *a2;

……

a1 = inet_ntoa(ina1.sin_addr); /* 这是198.92.129.1 */

a2 = inet_ntoa(ina2.sin_addr); /* 这是132.241.5.10 */

printf("address 1: %s\n",a1);

printf("address 2: %s\n",a2);

输出如下:

address 1: 132.241.5.10

address 2: 132.241.5.10

2、网络编程入门篇—— 服务端篇

在这里插入图片描述
TCP三次握手的Socket过程:非常非常非常重要

  • 1、服务器调用socket()、bind()、listen()完成初始化后,调用accept()阻塞等待;
  • 2、客户端Socket对象调用connect()向服务器发送了一个SYN并阻塞
  • 3、服务器完成了第一次握手,即发送SYN和ACK应答;
  • 4、客户端收到服务端发送的应答之后,从connect()返回,再发送一个ACK给服务器;
  • 5、服务器Socket对象接收客户端第三次握手ACK确认,此时服务端从accept()返回,建立连接。

在这里插入图片描述
CP四次挥手的Socket过程:

  • 1、某个应用进程调用close()主动关闭,发送一个FIN;
  • 2、另一端接收到FIN后被动执行关闭,并发送ACK确认;
  • 3、之后被动执行关闭的应用进程调用close()关闭Socket,并也发送一个FIN;
  • 4、接收到这个FIN的一端向另一端ACK确认。

在这里插入图片描述

具体状态变化图如下:
在这里插入图片描述

2.1、创建socket()

UNIX/Linux的一个哲学是:所有东西都是文件。 socket也不例外,它就是可读、可写、可控制、可关闭的文件描述符。下面的 socket系统调用可创建一个 socket:

#include 
#include 

int socket(int domain, int type, int protocol);

返回值:

  • 成功时返回一个 socket文件描述符,失败则返回-1并设置erno

参数说明:

  • 1、domain参数:告诉系统使用哪个底层协议族。对TCP/IP协议族而言,该参数应该设置为PF_INET( Protocol Family of Internet,用于IPv4)PF_INET6(用于IPv6):对于UNIX本地域协议族而言,该参数应该设置为 PF_UNIX
    2、type参数:指定服务类型。服务类型主要有SOCK_STREAM服务(流服务,TCP)和SOCK_UGRAM(数据报,UDP)服务。对TCP/IP协议族而言,其值取SOCK_STREAM表示传输层使用TCP协议,取 SOCK_DGRAM表示传输层使用UDP协议。

  • 3、protocol参数:是在前两个参数构成的协议集合下,再选择一个具体的协议。不过这个值通常都是唯一的(前两个参数已经完全决定了它的值)。几乎在所有情况下,我们都应该把它设置为0,表示使用默认协议。

参数更详细说明请参考

2.2、命名(又叫绑定)bind()

socket() 函数用来创建套接字,确定套接字的各种属性,然后服务器端要用 bind() 函数将套接字与特定的 IP 地址和端口绑定起来,只有这样,流经该 IP 地址和端口的数据才能交给套接字处理。类似地,客户端也要用 connect() 函数建立连接。

函数原型

#include 
#include 
int bind(int sock, struct sockaddr *addr, socklen_t addrlen); 

返回值:

  • 返回值为0时表示绑定成功,-1表示绑定失败,返回errno的错误值,相关errno如下:
    网络数据到底怎样的传输过程?什么是网络编程?一文教你清晰入门linux下socket网络编程—— 服务端篇(TCP协议传输)!_第2张图片

参数说明:

  • 1、sock:socket()函数创建的文件描述符。
  • 2、addr:指向一个结构为sockaddr参数的指针,sockaddr中包含了地址、端口和IP地址的信息。在进行地址绑定的时候,需要弦将地址结构中的IP地址、端口、类型等结构struct sockaddr中的域进行设置之后才能进行绑定,这样进行绑定后才能将套接字文件描述符与地址等接合在一起。
  • 3、addrlen:是addr结构的长度,可以设置成sizeof(struct sockaddr)。使用sizeof(struct sockaddr)来设置套接字的类型和其对已ing的结构。

使用:

if (bind(sockfd, (struct sockaddr *) &my_addr, sizeof(struct sockaddr)) == -1) 
{ // 判断是否绑定成功
    perror("bind");
    exit(EXIT_FAILURE);
 }

2.2.1、TIME_WAIT状态引起的bind失败的方法?

因为服务器的TCP连接没有完全断开之前是不允许重新监听,在某些情况下是不合理的。因为一般我们的服务器都会处理大量的客户端的连接,由于请求量很大所以可能导致TIME_WAIT的连接数很多,每个连接都会占用一个通信tuple.

所以我们可以使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1,表示允许创建端口号相同但是IP地址不同的多个socket描述符。
网络数据到底怎样的传输过程?什么是网络编程?一文教你清晰入门linux下socket网络编程—— 服务端篇(TCP协议传输)!_第3张图片

2.3、监听socket()(listen())

从listen函数的字面意思来看是用来监听客户端的连接,但是从接受连接来看,我们知道listen函数的本质其实是用来将主动socket转换为被动socket,同时限制服务端同一时刻所能接受客户端连接请求的个数。

另外, 如果套接字 sockfd 没有显示调用bind函数绑定指定的套接字地址的话,listen函数会选择本地ip地址,并随机选择一个端口号绑定到 sockfd上,但是一般作为服务器程序,通常会显示调用 bind 函数。
  
  当调用listen函数时,会让套接字从CLOSED状态转换为LISTEN状态

函数原型

#include 
#include 
int listen(int sockfd, int backlog); 

返回值:

  • 成功返回0,失败返回- 1。

参数说明:

  • 1、sockfd: 表示要设置的服务端套接字文件描述符
  • 2、backlog:表示要设置服务端套接字的未决连接队列的大小(注意:这个大小指的是同时建立三次握手连接的个数,包括已完成队列和未完成队列中的连接)。

关于backlog大小
1、客户端可能会在服务器调用accept之前调用connect,这种情况是有可能发生的,如果此时服务端可能正忙于处理其他客户端,这将产生一个未决连接。系统内核有一个未决连接队列会记录所有未决连接的信息,这样服务器在后面调用accept时就能够处理这些未决连接了,而backlog参数就是用来限制这种未决连接数量的。

2、如果未决连接队列已经满了,当接收到更多的连接请求就会忽略,也就是说客户端调用connect函数可能会阻塞,直到未决连接队列中的未决连接被accept为止(注意:当一个连接被accept时就会从未决连接队列删除,此时未决连队列有空位了)。
网络数据到底怎样的传输过程?什么是网络编程?一文教你清晰入门linux下socket网络编程—— 服务端篇(TCP协议传输)!_第4张图片

2.4、接受连接(accept(),重要)

1)accept函数负责从客户端“连接”的队列中返回一个最近的新连接。

2)accept只负责“接客”,不干别的,不是用于接收数据的,接收数据是通过read函数实现的,accept正常返回,就代表有新客户端连接(三次握手)。

函数原型

#include 
#include 
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);

返回值:

  • 成功时,返回非负整数,该整数是接收到套接字的描述符;
  • 出错时,返回-1,相应地设定全局变量errno。

参数说明:

  • 1、sockfd, 利用系统调用socket()建立的套接字描述符,通过bind()绑定到一个本地地址(一般为服务器的套接字),并且通过listen()一直在监听连接;
  • 2、addr, 指向struct sockaddr的指针,该结构用通讯层服务器对等套接字的地址(一般为客户端地址)填写若addr为NULL,没有有效地址填写,这种情况下,addrlen也不使用,应该置为NULL;
  • 3、addrlen, 一个值结果参数,调用函数必须初始化为包含addr所指向结构大小的数值,函数返回时包含对等地址(一般为服务器地址)的实际数值,设置为sizeof(struct sockaddr_in)

accept工作方式:阻塞和非阻塞

1、阻塞方式:blocking

  • 1、套接字也没有被标记为Non-blocking,accept()会阻塞调用函数直到连接出现,具体为:
    • 实现时accept()为阻塞函数,当监听socket调用accept()时,它先到自己的receive_buf中查看是否有连接数据包;
      • 若有,把数据拷贝出来,删掉接收到的数据包,创建新的socket与客户发来的地址建立连接;
      • 若没有,就阻塞等待;

accept阻塞使用实例

struct sockaddr_in server_sockaddr,client_sockaddr;
	int sin_size,recvbytes;
	int sockfd,client_fd;
	char buf[MAXDATASIZE];
/*创建socket*/
	if((sockfd = socket(AF_INET,SOCK_STREAM,0))==-1){
		perror("socket");
		exit(1);
	}
	printf("socket success!,sockfd=%d\n",sockfd);
/*设置服务器sockaddr_in结构*/
	server_sockaddr.sin_family=AF_INET;
	server_sockaddr.sin_port=htons(SERVPORT);
	server_sockaddr.sin_addr.s_addr=INADDR_ANY;
	bzero(&(server_sockaddr.sin_zero),8);
/*绑定socket和端口*/
	if(bind(sockfd,(struct sockaddr *)&server_sockaddr,sizeof(struct sockaddr))==-1){
		perror("bind");
		exit(1);
	}
	printf("bind success!\n");
/*监听客户端请求*/
	if(listen(sockfd,BACKLOG)==-1){
		perror("listen");
		exit(1);
	}
	printf("listening....\n");
/*接受客户端请求*/
	if((client_fd=accept(sockfd,(struct sockaddr *)&client_sockaddr,&sin_size))==-1){
		perror("accept");
		exit(1);
	}

2、非阻塞accept:非常重要
为了防止accept阻塞,套接字被标记为Non-blocking,当select监听的某个套接字有一个已完成连接正等待被accept时,把监听的套接字设置为非阻塞,然后调用accept忽略以下错误:

EWOULDBLOCK (Berkeley实现,客户端中止连接时)、 ECONNABORTED (POSIX实现,客户中止连接时)
EPROTO(SVR4实现,客户端中止连接时)EINTR(如果信号被捕获)

实现accept非阻塞:


//设置套接字非阻塞

int flags = fcntl(sock, F_GETFL, 0);
fcntl(listenfd , F_SETFL , flags|O_NONBLOCK);
while(1){
    //调用select函数
    FD_SET(listenfd , &rset);
    select(listenfd +1 , &rfds , NULL , NULL , ...);
    if(FD_ISSET(listenfd , &rset)){
    client_len = sizeof(client_addr);
    connfd = accept(listenfd , (struct sockaddr *)&client_addr , &client_len);
    if(connfd < 0){
        //忽略EWOULDBLOCK错误,继续循环
        if(errno == EWOULDBLOCK)
            continue;
        perror("accept");
        exit(-1);
        }
    }
}

小问题:socket编程accept队列获取的连接是三次握手成功的连接还是发送了syn包的连接?

  • 1、服务器调用socket()、bind()、listen()完成初始化后,调用accept()阻塞等待;
  • 2、客户端Socket对象调用connect()向服务器发送了一个SYN并阻塞
  • 3、服务器完成了第一次握手,即发送SYN和ACK应答;
  • 4、客户端收到服务端发送的应答之后,从connect()返回,再发送一个ACK给服务器;
  • 5、服务器Socket对象接收客户端第三次握手ACK确认,此时服务端从accept()返回,建立连接。

所以,三次握手之后,tcp连接会加入到accept队列。accept()会从队列中取一个连接返回,若队列为空,则阻塞。

  • Accept队列中存放的是已建立好的连接,也即等待被上层应用程序取走的连接。当进程调用accept(),这个socket从队列中取出,传递给上层应用程序。

2.4.1、connect()、listen()和accept()三者之间的关系

  • 1、connect()函数:客户端主动连接服务器,建立连接是通过三次握手。
    • 这个连接的过程是内核完成的,不是这个函数完成的,这个函数的作用仅仅是通知给linux内核,让linux内核自动完成TCP三次握手连接
    • 通常情况下,客户端的connect()函数会默认一直阻塞,直到三次握手成功或超市失败才返回
  • 2、listen函数:只要TCP服务器调用了listen(),客户端就可以通过connect()和服务器建立连接,而这个连接的过程是内核完成的
    • listen()函数不会阻塞
    • 在被动状态的socket有两个队列,一个是正在进行三次握手的socket队列,一个是完成三次握手的socket队列。在握手完成后会从正在握手队列移到握手完成的队列,此时已经建立连接。
  • 3、accept()函数:从连接队列头部取出一个已经完成的连接,如果这个队列没有已经完成的连接,accept()函数就会阻塞,直到取出队列中已完成的用户连接为止。

2.5、数据读写(send()+recv())

在这里插入图片描述

对文件的读写操作read和wrie同样适用于 socket但是 socket编程接口提供了几个专门用于 socket数据读写的系统调用,它们增加了对数据读写的控制。其中用于TCP流数据读写的系统调用是:

int send( SOCKET s,char *buf,int len,int flags );
int recv( SOCKET s, char *buf, int  len, int flags)

2.5.1、数据读(recv())

不论是客户还是服务器应用程序都用recv函数从TCP连接的另一端接收数据。recv函数,它并不是直接从网络中获取数据,而是从输入缓冲区中读取数据。

int recv( SOCKET s, char *buf, int  len, int flags)

返回值:

  • 返回数据的字节长度;若无可用数据或对等方已经按序结束,返回0;若出错,返回-1.

参数说明:

  • s:指定接收端套接字描述符;
  • buf:指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;
  • len:指明buf的长度;
  • flags :一般置为0。

recv函数的执行流程

  • 1、当应用程序调用recv函数时,recv先等待s的发送缓冲 中的数据被协议传送完毕,

  • 2、如果协议在传送s的发送缓冲中的数据时出现网络错误,那么recv函数返回SOCKET_ERROR

  • 3、如果s的发送缓冲中没有数 据或者数据被协议成功发送完毕后,recv先检查套接字s的接收缓冲区,如果s接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,只到 协议把数据接收完毕。

  • 4、当协议把数据接收完毕,recv函数就把s的接收缓冲中的数据copy到buf中(注意协议接收到的数据可能大于buf的长度,所以 在这种情况下要调用几次recv函数才能把s的接收缓冲中的数据copy完。 )

  • 5、recv函数仅仅是copy数据,真正的接收数据是协议来完成的,recv函数返回其实际copy的字节数。

  • 6、如果recv在copy时出错,那么它返回SOCKET_ERROR;如果recv函数在等待协议接收数据时网络中断了,那么它返回0。

2.5.2、数据写(send())

不论是客户还是服务器应用程序都用send函数来向TCP连接的另一端发送数据。

int send( SOCKET s,char *buf,int len,int flags );

返回值:

  • send函数copy数据成功,就返回实际copy的字节数,
  • 如果send在copy数据时出现错误,那么send就返回SOCKET_ERROR;
  • 如果send在等待协议传送数据时网络断开的话,那么send函数也返回SOCKET_ERROR。

参数说明:

  • s:指定接收端套接字描述符;
  • buf:指明一个缓冲区,一个存放应用程序要发送数据的缓冲区;
  • len:实际要发送的数据的字节数;
  • flags :一般置为0。

send函数的执行流程

  • 1、当调用该函数时,send先比较待发送数据的长度len和套接字s的发送缓冲的 长度
    • 如果len大于s的发送缓冲区的长度,该函数返回SOCKET_ERROR
    • 如果len小于或者等于s的发送缓冲区的长度,那么send先检查协议 是否正在发送s的发送缓冲中的数据
      • 如果是,就等待协议把数据发送完,
      • 如果还没有开始发送s的发送缓冲中的数据或者s的发送缓冲中没有数据,那么 send就比较s的发送缓冲区的剩余空间和len
        • 如果len大于剩余空间大小,send就一直等待协议把s的发送缓冲中的数据发送完
        • 如果len小于剩余空间大小,send就仅仅把buf中的数据copy到剩余空间里(注意并不是send把s的发送缓冲中的数据传到连接的另一端的,而是协议传的,send仅仅是把buf中的数据copy到s的发送缓冲区的剩余空间里)。
  • 2、如果send函数copy数据成功,就返回实际copy的字节数,
  • 3、如果send在copy数据时出现错误,那么send就返回SOCKET_ERROR;
  • 4、如果send在等待协议传送数据时网络断开的话,那么send函数也返回SOCKET_ERROR。

注意

  • send函数并不是直接将数据传输到网络中,而是负责将数据写入输出缓冲区数据从输出缓冲区发送到目标主机是由TCP协议完成的。数据写入到输出缓冲区之后,send函数就可以返回了,数据是否发送出去,是否发送成功,何时到达目标主机,都不由它负责了,而是由协议负责。
  • 如 果协议在后续的传送过程中出现网络错误的话,那么下一个Socket函数就会返回SOCKET_ERROR。
				char buffer[1024];
                int len;
                if ((len=recv(events[i].data.fd,buffer,sizeof(buffer), 0))>0)
                {
                    send(events[i].data.fd,"Welcome to My server\n",21,0);
                    printf("%s fd %d \n",buffer,events[i].data.fd);
                    
                    //close(client_fd);
                }else{
                    printf("client offline with: "
                           "clientfd = %d \n",
                           events[i].data.fd);
                }

2.6、关闭连接 (close() and shutdown())

2.6.1、close()

关闭一个连接实际上就是关闭该连接对应的 socket,这可以通过如下关闭普通文件描述系统调用来完。

#include 
int close( int fd );

返回值:

  • 成功则返回0,错误返回-1,
  • 错误码errno:
    • EBADF表示fd不是一个有效描述符;
    • EINTR表示close函数被信号中断;
    • EIO表示一个IO错误。

参数说明:

  • fd参数:待关闭的 socket。
    • 不过, close系统调用并非总是立即关闭一个连接,而是将fd的引用计数减1。只有当fd的引用计数为0时,才真正关闭连接
    • 多进程程序中,一次fork系统调用默认将使父进程中打开的 socket的引用计数加1,因此我们必须在父进程和子进程中都对该 socket执行 close调用才能将连接关闭

2.6.2、shutdown()

如果无论如何都要立即终止连接(而不是将 socket的引用计数减1),可以使用如下的shutdown系统调用,并且如果你想在如何关闭套接字上有多一点的控制,你可以使用函数 shutdown()。它允许你将一定方向上的通讯或者双向的通讯(就象close()一 样)关闭。

#include
int shutdown(int sockfd,int how);

返回值

  • 成功则返回0,
  • 错误返回-1,错误码errno:
    • EBADF表示sockfd不是一个有效描述符;
    • ENOTCONN表示sockfd未连接;
    • ENOTSOCK表示sockfd是一个文件描述符而不是socket描述符。

参数说明:

  • sockfd参数:是待关闭的 socket.
  • how参数:决定了 shutdown的行为,它可取下表中的某个值。0 不能再读,1不能再写,2 读写都不能
    在这里插入图片描述
  • SHUT_RD(0)关闭sockfd上的读功能,此选项将不允许sockfd进行读操作。即该套接字不再接受数据,任何当前在套接字接受缓冲区的数据将被丢弃。进程将不能对该套接字发出任何读操作。对TCP套接字该调用之后接受到的任何数据将被确认然后无声的丢弃掉。
  • SHUT_WR(1)关闭sockfd的写功能,此选项将不允许sockfd进行写操作,即进程不能在对此套接字发出写操作。
  • SHUT_RDWR(2)关闭sockfd的读写功能,相当于调用shutdown两次:首先是以SHUT_RD,然后以SHUT_WR。

注意:

  • 1、shutdown()的效果是累计的,不可逆转的。
    • 即如果关闭了一个方向数据传输,那么这个方向将会被关闭直至完全被关闭或删除,而不能重新被打开。
    • 如果第一次调用了shutdown(0),第二次调用了shutdown(1),那么这时的效果就相当于shutdown(2),也就是双向关闭socket。
  • 2、shutdown与socket描述符没有关系,即使调用shutdown(fd, SHUT_RDWR)也不会关闭fd,最终还需close(fd)。
  • 3、如果在无连接的数据报套接字中使用shutdown(),那么只不过是让 send() 和 recv() 不能使用

应用场景:优雅的关闭

通常来说,socket是双向的,即数据是双向通信的。但有些时候,你会想在socket上实现单向的socket,即数据往一个方向传输。单向的socket便称为半开放Socket。要实现半开放式,需要用到shutdown()函数。

一般来说,半开放socket适用于以下场合:

  • 1.当你想要确保所有写好的数据已经发送成功时。如果在发送数据的过程中,网络意外断开或者出现异常,系统不一定会返回异常,这是你可能以为对端已经接收到数据了。这时需要用shutdown()来确定数据是否发送成功,因为调用shutdown()时只有在缓存中的数据全部发送成功后才会返回。
  • 2.想用一种方法来捕获程序潜在的错误,这错误可能是因为往一个不能写的socket上写数据,也有可能是在一个不该读操作的socket上读数据。当程序尝试这样做时,将会捕获到一个异常,捕获异常对于程序排错来说是相对简单和省劲的。
  • 3.当您的程序使用了fork()或者使用多线程时,你想防止其他线程或进程访问到该资源,又或者你想立刻关闭这个socket,那么可以用shutdown()来实现。

2.6.3、shutdown()和close()区别

1、close

  • close函数函数会关闭套接字,如果由其他进程共享着这个套接字,那么它仍然是打开的,这个连接仍然可以用来读和写。
  • close则立即双方向强制关闭socket并释放相关资源。

2、shutdown

  • 切断进程共享的套接字的所有连接,不管引用计数是否为0,由第二个参数选择断连的方式。
  • shutdown是一种优雅地单方向或者双方向关闭socket的方法

更多区别请参考

3、网络编程入门篇——客户端篇

篇幅原因,上一篇博客说:

网络数据到底怎样的传输过程?什么是网络编程?一文教你简单入门 linux下socket网络编程 —— 客户端篇(TCP协议传输)!

具体结构如下:

2.1、创建socket()

2.2、发起连接(connect(),面试常问)

2.3、数据读写(send()+recv())

2.3.1、数据读(recv())

2.3.2、数据写(send())

2.4、关闭连接 (close() and shutdown())

2.4.1、close()

2.4.2、shutdown()

2.4.3、shutdown()和close()区别

4、linux socket实现简单的服务器和客户端对话

server.c

1.服务器端
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define SERVPORT 3333
#define BACKLOG 10
#define MAX_CONNECTED_NO 10
#define MAXDATASIZE 5
 
int main()
{
	struct sockaddr_in server_sockaddr,client_sockaddr;
	int sin_size,recvbytes;
	int sockfd,client_fd;
	char buf[MAXDATASIZE];
/*创建socket*/
	if((sockfd = socket(AF_INET,SOCK_STREAM,0))==-1){
		perror("socket");
		exit(1);
	}
	printf("socket success!,sockfd=%d\n",sockfd);
/*设置服务器sockaddr_in结构*/
	server_sockaddr.sin_family=AF_INET;
	server_sockaddr.sin_port=htons(SERVPORT);
	server_sockaddr.sin_addr.s_addr=INADDR_ANY;
	bzero(&(server_sockaddr.sin_zero),8);
/*绑定socket和端口*/
	if(bind(sockfd,(struct sockaddr *)&server_sockaddr,sizeof(struct sockaddr))==-1){
		perror("bind");
		exit(1);
	}
	printf("bind success!\n");
/*监听客户端请求*/
	if(listen(sockfd,BACKLOG)==-1){
		perror("listen");
		exit(1);
	}
	printf("listening....\n");
/*接受客户端请求*/
	if((client_fd=accept(sockfd,(struct sockaddr *)&client_sockaddr,&sin_size))==-1){
		perror("accept");
		exit(1);
	}
/*接收客户端信息*/
	if((recvbytes=recv(client_fd,buf,MAXDATASIZE,0))==-1){
		perror("recv");
		exit(1);
	}
 	printf("received a connection :%s\n",buf);
/*关闭socket*/
	close(sockfd);
}

client.c

2.客户端
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#define SERVPORT 3333
#define MAXDATASIZE 100

int main(int argc,char *argv[]){
	int sockfd,sendbytes;
	char buf[MAXDATASIZE];
	struct hostent *host;
	struct sockaddr_in serv_addr;
/*argc<2,表示没有输入主机名,主机句是IP地址形式,如“192.168.1.1”*/
	if(argc < 2){
		fprintf(stderr,"Please enter the server's hostname!\n");
		exit(1);
	}
/*获取主机名,地址解析函数*/
	if((host=gethostbyname(argv[1]))==NULL){
		perror("gethostbyname");
		exit(1);
	}
/*创建socket*/
	if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1){
		perror("socket");
		exit(1);
	}
/*设置serv_addr结构参数*/
	serv_addr.sin_family=AF_INET;
	serv_addr.sin_port=htons(SERVPORT);
	serv_addr.sin_addr=*((struct in_addr *)host->h_addr);
	bzero(&(serv_addr.sin_zero),8);
/*向服务器请求连接,serv_addr是服务器端地址*/
	if(connect(sockfd,(struct sockaddr *)&serv_addr,sizeof(struct sockaddr))==-1)
	{
		perror("connect");
		exit(1);
	}
/*发送消息给服务器,此时可以在服务器端看到"hello"字样*/
	if((sendbytes=send(sockfd,"hello",5,0))==-1){
		perror("send");
		exit(1);
	}
/*关闭连接*/
	close(sockfd);
}
 

编译运行:开两个终端

#gcc server.c -o server
#./server                       //此时服务器端在监听
#gcc client.c -o client
#./client yourIP        //客户端向服务器端发送“hello",服务器端监听终止

参考

1、https://bbs.gameres.com/thread_842984_1_1.html
2、https://www.cnblogs.com/kefeiGame/p/7246942.html
3、《高性能网络编程》——游双
4、https://blog.csdn.net/qq_43412060/article/details/107140216?from=singlemessage
5、https://zhuanlan.zhihu.com/p/109826876
6、https://www.cnblogs.com/xingguang1130/p/11643446.html
7、https://zhuanlan.zhihu.com/p/112312104
8、https://www.cnblogs.com/wanpengcoder/p/5356776.html

你可能感兴趣的:(网络编程,listen,bind,accept,socket)