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

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 <arpa/inet.h>

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用于函数参数。

实例

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>

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 <arpa/inet.h>

//将主机字节序转换为网络字节序
 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()返回,建立连接。

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

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

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

2.1、创建socket()

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

#include <sys/types.h>
#include <sys/socket.h>

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、发起连接(connect(),面试常问)

如果说服务器通过sten调用来被动接受连接,那么客户端需要通过如下系统调用来主动与服务器建立连接
函数原型

#include <sys/types.h>
#include <sys/socket.h>

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

返回值:

  • 成功时返回0。一旦成功建立连接, sockfd就唯一地标识了这个连接,客户端就可以通过读写 sockfd来与服务器通信。

  • 失败则返回-1,并设置erno。其中两种常见的ermo是 ECONNREFUSEDETIMEDOUT,它们的含义如下:

  • 1、 ECONNREFUSED,目标端口不存在,连接被拒绝。

  • 2、ETIMEDOUT,连接超时。

参数说明:

  • 1、sockfd参数:由 socket系统调用返回一个 socket.
  • 2、serv_addr参数:是服务器监听的 socket地址, 即目的地端口和 IP 地址的数据结构 struct sockaddr
  • 3、addrlen参数:则指定这个地址的长度,即sizeof(struct sockaddr)
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>

#define DEST_IP "132.241.5.10"
#define DEST_PORT 23

main()

{
  int sockfd;
  struct sockaddr_in dest_addr; /* 目的地址*/
  sockfd = socket(AF_INET, SOCK_STREAM, 0); /* 错误检查 */
  dest_addr.sin_family = AF_INET; /* host byte order */
  dest_addr.sin_port = htons(DEST_PORT); /* short, network byte order */
  dest_addr.sin_addr.s_addr = inet_addr(DEST_IP);
  bzero(&(dest_addr.sin_zero),; /* zero the rest of the struct */
  /* don't forget to error check the connect()! */
  connect(sockfd, (struct sockaddr *)&dest_addr, sizeof(struct sockaddr));
  ……
 }

connect工作方式:阻塞+非阻塞

1、阻塞模式下,connect的返回结果

客户端调用connect()函数将激发TCP的三路握手过程,但仅在连接建立成功或出错时才返回(这一点和非阻塞区分很重要)

  • (1)若TCP客户端没有收到syn分节的响应,则返回ETIMEOUT错误;调用connect函数时,内核发送一个syn,若无响应则等待6s后再发送一个,若仍然无响应则等待24s后在发送一个,若总共等待75s后仍未收到响应则返回本错误;
  • (2)若对客户的syn响应是RST,则表明该服务器在我们指定的端口上没有进程在等待与之连接,这是一种硬错误,客户一收到RST马上返回ECONNREFUSED错误; 产生RST的三种情况:
    • 1、是SYN到达某端口但此端口上没有正在侦听的服务器、
    • 2、是TCP想取消一个已有连接、
    • 3、是TCP接收了一个根本不存在的连接上的
  • (3)若客户发送的syn在中间的某个路由器上引发了目的不可达icmp错误,则认为是一种软错误。客户主机内核保存该消息,并按照第一种情况的时间间隔继续发送syn,咋某个规定时间后仍未收到响应,则把保存的消息作为EHOSTUNREACH或者ENETUNREACH错误返回给进程;

2、非阻塞工作模式

  • 1、调用connect()函数会立刻返回EINPROCESS错误,但TCP通信的三路握手过程正在进行,所以可以使用select函数来检查这个连接是否建立成功。
    • 当连接成功建立时,描述字变成可写。
    • 当连接建立出错时,描述字变成即可读又可写。getsockopt()函数的errno == 0表示只可写。
	int socfd = socket();
	//set non-blocking
	int ret = connect(sockfd, ...);
	if(ret < 0)
	{
	    return -1;
	}
	if(errno == EINPRPGRESS)
	{
	    fd_set rdset, wrset;
	    FD_SET(sockfd, rdset);
	    FD_SET(sockfd, wrset);
	    ret = select(sockfd+1, &rdset, &wrset, NULL, timeout);
	    if(FD_ISSET(sockfd, &wrset))
	    {
	        if(FD_ISSET(sockfd, rdset))
	        {
	            //清楚sock error
	        }
	        //成功连接
	        return sockfd.
	    }
	}

非阻塞connect的意义

非阻塞connect的意义在于提高并发度。阻塞connect下,完成一个三次握手需要耗费一个RTT时间。RTT时间波动很大。从局域网内的几时毫秒到广域网的几十秒。阻塞模式下,进程被connect阻塞住,什么都干不了。非阻塞下,我们可以让select或者epoll来监听listenfd,直到完成三次连接再继续进行数据的手法。

linux-socket connect阻塞和非阻塞模式 示例

2.3、数据读写(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.3.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.3.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.4、关闭连接 (close() and shutdown())

2.4.1、close()

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

#include <unistd. h>
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.4.2、shutdown()

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

#include<sys/socket.h>
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.4.3、shutdown()和close()区别

1、close

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

2、shutdown

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

更多区别请参考

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

篇幅问题:下一个博客继续说明,具体结构如下
网络数据到底怎样的传输过程?什么是网络编程?一文教你清晰入门linux下socket网络编程—— 服务端篇(TCP协议传输)!

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()区别

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

server.c

1.服务器端
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>

#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 <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#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、《高性能网络编程》——游双
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

  • 13
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值