TCP套接字编程

套接字(socket)编程

       在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识网络通讯中的一个进程,“IP地址+端口号”就叫做套接字。

       在TCP协议中,建立连接的两个进程各自有一个套接字来标识,那么这两个套接字组成的socket pair就唯一标示一个连接。“socket”本身有“插座”的意思,用来描述网络连接中的一对一关系。

       为TCP/IP协议设计的应用层编程接口称为socket API。

       下面介绍TCP协议以及UDP协议的函数接口。

TCP套接字

大小端

       内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件的偏移地址也有大端小端之分。网络数据流同样有大端小端之分,网络数据流的地址规定:先发出去的数据是低地址,后发出去的数据是高地址。

       TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。例如上一节的UDP段格式中,地址0~1是十六位的源端口号,如果这个端口号是1000(0x3e8),则地址0是0x03,地址1是0xe8,先发地址0,后发地址1。但如果发送主机是小端字节序的,这16位就会被解释成0x3e8而不是1000。因此,这中间要进行字节序的转换。

       进行网络字节序和主机字节序的转换可以调用一下库函数:

 

       h表示host

       n表示network

       l表示32位长整数

       s表示16位短整数。例如htonl表示将32位的长整数从主机字节序转换成网络字节序。

     socket地址的数据类型及相关函数socket API是一层抽象的网络编程接口,适用于各种底层网络协议。然而,各种网络协议的地址格式并不相同。

sockaddr数据结构:

        

       IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,IPv6地址用sockaddr_in6结构体表示。UNIX Domain Socket的地址格式定义在sys/un.h中,用sockaddr_un结构体表示。IPv4、IPv6和UNIXDomain Socket的地 址类型分别定义为常数AF_INET、AF_INET6、AF_UNIX。它们的结构不尽相同,但开头是一样的。这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。因此,socket API可以接受各种类型的sockaddr结构体指针做参数,例如bind、accept、connect等函数,这些函数的参数应该设计成void *类型以便接受各种类型的指针,但是sock API的实现早于ANSI C标准化,那时还没有void *类型,因此这些函数的参数都用struct sockaddr *类型表示,在传递参数之前要强制类型转换一下:

         

       基于IPv4的socket网络编程中,sockaddr_in中的成员struct  in_addr  sin_addr表示32位 的IP 地址。但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示和in_addr表示之间转换。

字符串转点分十进制的函数:

         

TCP协议通讯流程:

       服务器:调用socket()、bind()、listen()完成初始化后,调用accept()阻塞等待,处于监听端口的状态。

       客户端:调用socket()初始化完成后,调用connect()发出SYN段并阻塞等待服务器应答。服务器应答一个SYN-ACK段,客户端收到后从connect()返回,同时应答一个ACK段。

       服务器:收到客户端收到的ACK后从accept()返回。

简单的TCP网络程序

tcp_server.c

         

        

        

       程序中用到的socket API有:

       <1>

        

       socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以像读写文件一样用read/write在网络上收发数据,如果socket()调用出错则返回-1。对于IPv4,family参数指定为AF_INET。对于TCP协议,type参数指定为SOCK_STREAM,表示面向流的传输协议。

        <2>

        

       bind()的作用是将参数sockfd和myaddr绑定在⼀一起,使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号。前面讲过,struct sockaddr *是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数 addrlen指定结构体的长度。我们的程序中对myaddr参数是这样初始化的:

local.sin_family =AF_INET;

local.sin_port =htons(_port);

local.sin_addr.s_addr =inet_addr(_ip);

       <3>

        

       当有客户端发起连接时,服务器调用的accept() 返回并接受这个连接,如果有大量的客户端发起连接而服务器来不及处理,尚未accept的客户端就处于连接等待状态,listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接等待状态,如果接收到更多的连接请求就忽略。listen()成功返回0,失败返回-1。

        <4>

        

       三方握⼿手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接。addr是一个传出参数,accept()返回时传出客户端的地址和端口号。addrlen参数是一个传入传出参数(value-result argument),传入的是调用者提供的缓冲区addr 的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给addr 参数传NULL,表示不关心客户端的地址。

tcp_client.c

        

        

       由于客户端不需要固定的端口号,所以不用bind(),客户端的端口号由内核⾃自动分配。服务器也不是必须调用bind(),但如果服务器不调用bind(),内核会自动给服务器分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦。

        

       客户端需要调用connect()连接服务器,connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址。connect()成功返回0,出错返回-1。

       先编译运行服务器,再运行客户端,给服务器发消息:

        

       服务器回应消息:

        

       现在用Ctrl C使 server终止,这时马上再运行server,结果是:

        bind error: Address already in use

        
       这是因为,虽然server的应用程序终止了,但TCP协议层的连接并没有完全断开,因此不能再次监听同样的server端口。现在用Ctrl C把client也终止。client终止时自动关闭socket描述符,server的TCP连接收到client发的 FIN段后处于TIME_WAIT状态。

       TCP协议规定,主动关闭连接的一方要处于TIME_WAIT状态,等待两个MSL时间后才能回到CLOSED状态,因为我们先Ctrl-C终止了server,所以server是主动关闭连接的一方,在TIME_WAIT期间仍然不能再次监听同样的server端口。

       可以用netstat -nltp找到它的PID,然后用kill命令将它杀掉,就可以重新绑定了:

        

猜你喜欢

转载自blog.csdn.net/wxt_hillwill/article/details/73723750