socket io(1)、Linux的socket编程详解_linux socket

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以点击这里获取!

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

/* Internet address. /
struct in_addr {
uint32_t s_addr; /
address in network byte order */
};

 ipv6对应的是:  

struct sockaddr_in6 {
sa_family_t sin6_family; /* AF_INET6 /
in_port_t sin6_port; /
port number /
uint32_t sin6_flowinfo; /
IPv6 flow information /
struct in6_addr sin6_addr; /
IPv6 address /
uint32_t sin6_scope_id; /
Scope ID (new in 2.4) */
};

struct in6_addr {
unsigned char s6_addr[16]; /* IPv6 address */
};

 Unix域对应的是:  

#define UNIX_PATH_MAX 108

struct sockaddr_un {
sa_family_t sun_family; /* AF_UNIX /
char sun_path[UNIX_PATH_MAX]; /
pathname */
};

* addrlen:对应的是地址的长度。


通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。



> 
> #### 网络字节序与主机字节序
> 
> 
> **主机字节序**就是我们平常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。引用标准的Big-Endian和Little-Endian的定义如下:
> 
> 
>   a) Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
> 
> 
>   b) Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
> 
> 
> **网络字节序**:4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。**由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。**字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序的问题了。
> 
> 
> 所以:在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是Big-Endian。由于这个问题曾引发过血案!公司项目代码中由于存在这个问题,导致了很多莫名其妙的问题,所以请谨记对主机字节序不要做任何假定,务必将其转化为网络字节序再赋给socket。
> 
> 
> 


### 4.3、listen()、connect()函数: 监听和连接


如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。



int listen(int sockfd, int backlog);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);


listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。


connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。


### 4.4、accept()函数:接收请求


TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就向TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。



int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //返回连接connect_fd



参数sockfd


参数sockfd就是上面解释中的监听套接字,这个套接字用来监听一个端口,当有一个客户与服务器连接时,它使用这个一个端口号,而此时这个端口号正与这个套接字关联。当然客户不知道套接字这些细节,它只知道一个地址和一个端口号。


参数addr


这是一个结果参数,它用来接受一个返回值,这返回值指定客户端的地址,当然这个地址是通过某个地址结构来描述的,用户应该知道这一个什么样的地址结构。如果对客户的地址不感兴趣,那么可以把这个值设置为NULL。


参数len


如同大家所认为的,它也是结果的参数,用来接受上述addr的结构的大小的,它指明addr结构所占有的字节个数。同样的,它也可以被设置为NULL。



如果accept成功返回,则服务器与客户已经正确建立连接了,此时服务器通过accept返回的套接字来完成与客户的通信。


注意:


      accept默认会阻塞进程,直到有一个客户连接建立后返回,它返回的是一个新可用的套接字,这个套接字是连接套接字。


此时我们需要区分两种套接字,


       监听套接字: 监听套接字正如accept的参数sockfd,它是监听套接字,在调用listen函数之后,是服务器开始调用socket()函数生成的,称为监听socket描述字(监听套接字)


       连接套接字:一个套接字会从主动连接的套接字变身为一个监听套接字;而accept函数返回的是已连接socket描述字(一个连接套接字),它代表着一个网络已经存在的点点连接。


        一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。


        自然要问的是:为什么要有两种套接字?原因很简单,如果使用一个描述字的话,那么它的功能太多,使得使用很不直观,同时在内核确实产生了一个这样的新的描述字。


连接套接字socketfd\_new 并没有占用新的端口与客户端通信,依然使用的是与监听套接字socketfd一样的端口号


### 4.5、read()、write()等函数:读取和写入数据


万事具备只欠东风,至此服务器与客户已经建立好连接了。可以调用网络I/O进行读写操作了,即实现了网咯中不同进程之间的通信!网络I/O操作有下面几组:


* read()/write()
* recv()/send()
* readv()/writev()
* recvmsg()/sendmsg()
* recvfrom()/sendto()


我推荐使用recvmsg()/sendmsg()函数,这两个函数是最通用的I/O函数,实际上可以把上面的其它函数都替换成这两个函数。它们的声明如下:



   #include <unistd.h>

   ssize_t read(int fd, void *buf, size_t count);
   ssize_t write(int fd, const void *buf, size_t count);

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

   ssize_t send(int sockfd, const void *buf, size_t len, int flags);
   ssize_t recv(int sockfd, void *buf, size_t len, int flags);

   ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                  const struct sockaddr *dest_addr, socklen_t addrlen);
   ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                    struct sockaddr *src_addr, socklen_t *addrlen);

   ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
   ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

read函数是负责从fd中读取内容.当读成功时,read返回实际所读的字节数,如果返回的值是0表示已经读到文件的结束了,小于0表示出现了错误。如果错误为EINTR说明读是由中断引起的,如果是ECONNREST表示网络连接出了问题。


write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节数。失败时返回-1,并设置errno变量。 在网络程序中,当我们向套接字文件描述符写时有俩种可能。1)write的返回值大于0,表示写了部分或者是全部的数据。2)返回的值小于0,此时出现了错误。我们要根据错误类型来处理。如果错误为EINTR表示在写的时候出现了中断错误。如果为EPIPE表示网络连接出现了问题(对方已经关闭了连接)。


其它的我就不一一介绍这几对I/O函数了,具体参见man文档或者baidu、Google,下面的例子中将使用到send/recv。


### 4.6、close()函数:关闭连接


在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。



#include <unistd.h>
int close(int fd);


close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。


注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。




## 五. Socket中TCP的建立(三次握手)




---


TCP协议通过三个报文段完成连接的建立,这个过程称为三次握手(three-way handshake),过程如下图所示。



第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN\_SEND状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。


第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN\_RECV状态;  
 第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。  
 一个完整的三次握手也就是: 请求---应答---再次确认。


对应的函数接口:  
        ![](https://img-blog.csdnimg.cn/59285e49ff724556a49ce5bf746e8b0b.png)



从图中可以看出,当客户端调用connect时,触发了连接请求,向服务器发送了SYN J包,这时connect进入阻塞状态;服务器监听到连接请求,即收到SYN J包,调用accept函数接收请求向客户端发送SYN K ,ACK J+1,这时accept进入阻塞状态;客户端收到服务器的SYN K ,ACK J+1之后,这时connect返回,并对SYN K进行确认;服务器收到ACK K+1时,accept返回,至此三次握手完毕,连接建立。



我们可以通过网络抓包的查看具体的流程:


比如我们服务器开启9502的端口。使用tcpdump来抓包:



**tcpdump -iany tcp port 9502**



然后我们使用telnet 127.0.0.1 9502开连接.:


telnet 127.0.0.1 9502



14:12:45.104687 IP localhost.39870 > localhost.9502: Flags [S], seq 2927179378, win 32792, options [mss 16396,sackOK,TS val 255474104 ecr 0,nop,wscale 3], length 0**(1)**  
 14:12:45.104701 IP localhost.9502 > localhost.39870: Flags [S.], seq 1721825043, ack 2927179379, win 32768, options [mss 16396,sackOK,TS val 255474104 ecr 255474104,nop,wscale 3], length 0  **(2)**  
 14:12:45.104711 IP localhost.39870 > localhost.9502: Flags [.], ack 1, win 4099, options [nop,nop,TS val 255474104 ecr 255474104], length 0  **(3)**


  
 14:13:01.415407 IP localhost.39870 > localhost.9502: Flags [P.], seq 1:8, ack 1, win 4099, options [nop,nop,TS val 255478182 ecr 255474104], length 7  
 14:13:01.415432 IP localhost.9502 > localhost.39870: Flags [.], ack 8, win 4096, options [nop,nop,TS val 255478182 ecr 255478182], length 0  
 14:13:01.415747 IP localhost.9502 > localhost.39870: Flags [P.], seq 1:19, ack 8, win 4096, options [nop,nop,TS val 255478182 ecr 255478182], length 18  
 14:13:01.415757 IP localhost.39870 > localhost.9502: Flags [.], ack 19, win 4097, options [nop,nop,TS val 255478182 ecr 255478182], length 0



* 114:12:45.104687 时间带有精确到微妙
* localhost.39870 > localhost.9502 表示通信的流向,39870是客户端,9502是服务器端
* [S] 表示这是一个SYN请求
* [S.] 表示这是一个SYN+ACK确认包:
* [.] 表示这是一个ACT确认包, (client)SYN->(server)SYN->(client)ACT 就是3次握手过程
* [P] 表示这个是一个数据推送,可以是从服务器端向客户端推送,也可以从客户端向服务器端推
* [F] 表示这是一个FIN包,是关闭连接操作,client/server都有可能发起
* [R] 表示这是一个RST包,与F包作用相同,但RST表示连接关闭时,仍然有数据未被处理。可以理解为是强制切断连接
* win 4099 是指滑动窗口大小
* length 18指数据包的大小




我们看到 **(1)**(2)**(3)三步是建立tcp:******


******第一次握手:******


14:12:45.104687 IP localhost.39870 > localhost.9502: Flags [S], seq 2927179378


客户端IP localhost.39870 (客户端的端口一般是自动分配的) 向服务器localhost.9502 发送syn包(syn=j)到服务器》


syn包(syn=j) : syn的seq= 2927179378  (j=2927179378)



**第二次握手:**


14:12:45.104701 IP localhost.9502 > localhost.39870: Flags [S.], seq 1721825043, ack 2927179379,


收到请求并确认:服务器收到syn包,并必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包:  
 此时服务器主机自己的SYN:seq:y= syn seq 1721825043。  
 ACK为j+1 =(ack=j+1)=ack 2927179379   
  
  


**第三次握手:**


14:12:45.104711 IP localhost.39870 > localhost.9502: Flags [.], ack 1,


客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1)



客户端和服务器进入ESTABLISHED状态后,可以进行通信数据交互。此时和accept接口没有关系,即使没有accepte,也进行3次握手完成。


连接出现连接不上的问题,一般是网路出现问题或者网卡超负荷或者是连接数已经满啦。



紫色背景的部分:


IP localhost.39870 > localhost.9502: Flags [P.], seq 1:8, ack 1, win 4099, options [nop,nop,TS val 255478182 ecr 255474104], length 7


客户端向服务器发送长度为7个字节的数据,



IP localhost.9502 > localhost.39870: Flags [.], ack 8, win 4096, options [nop,nop,TS val 255478182 ecr 255478182], length 0


服务器向客户确认已经收到数据



 IP localhost.9502 > localhost.39870: Flags [P.], seq 1:19, ack 8, win 4096, options [nop,nop,TS val 255478182 ecr 255478182], length 18


然后服务器同时向客户端写入数据。



 IP localhost.39870 > localhost.9502: Flags [.], ack 19, win 4097, options [nop,nop,TS val 255478182 ecr 255478182], length 0


客户端向服务器确认已经收到数据



这个就是tcp可靠的连接,每次通信都需要对方来确认。




### 


## 六. TCP连接的终止(四次握手释放)




---


建立一个连接需要三次握手,而终止一个连接要经过四次握手,这是由TCP的半关闭(half-close)造成的,如图:


![](https://img-blog.csdnimg.cn/98b9397580584b539e634af7be722697.png)




由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这个原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。


(1)客户端A发送一个FIN,用来关闭客户A到服务器B的数据传送(报文段4)。


(2)服务器B收到这个FIN,它发回一个ACK,确认序号为收到的序号加1(报文段5)。和SYN一样,一个FIN将占用一个序号。


(3)服务器B关闭与客户端A的连接,发送一个FIN给客户端A(报文段6)。


(4)客户端A发回ACK报文确认,并将确认序号设置为收到序号加1(报文段7)。


对应函数接口如图:


![](https://img-blog.csdnimg.cn/1f2db2aaf2884daa987e844adbd1f849.png)


过程如下:


* 某个应用进程首先调用close主动关闭连接,这时TCP发送一个FIN M;
* 另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据;
* 一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N;
* 接收到这个FIN的源发送端TCP对它进行确认。


这样每个方向上都有一个FIN和ACK。


1.为什么建立连接协议是三次握手,而关闭连接却是四次握手呢?


这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的建连请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可以未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。



2.为什么TIME\_WAIT状态还需要等2MSL后才能返回到CLOSED状态?


这是因为虽然双方都同意关闭连接了,而且握手的4个报文也都协调和发送完毕,按理可以直接回到CLOSED状态(就好比从SYN\_SEND状态到ESTABLISH状态那样);但是因为我们必须要假想网络是不可靠的,你无法保证你最后发送的ACK报文会一定被对方收到,因此对方处于LAST\_ACK状态下的SOCKET可能会因为超时未收到ACK报文,而重发FIN报文,所以这个TIME\_WAIT状态的作用就是用来重发可能丢失的ACK报文。



### 


## 七. Socket编程实例




---


服务器端:一直监听本机的8000号端口,如果收到连接请求,将接收请求并接收客户端发来的消息,并向客户端返回消息。




/* File Name: server.c /
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#define DEFAULT_PORT 8000
#define MAXLINE 4096
int main(int argc, char
* argv)
{
int socket_fd, connect_fd;
struct sockaddr_in servaddr;
char buff[4096];
int n;
//初始化Socket
if( (socket_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1 ){
printf(“create socket error: %s(errno: %d)\n”,strerror(errno),errno);
exit(0);
}
//初始化
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//IP地址设置成INADDR_ANY,让系统自动获取本机的IP地址。
servaddr.sin_port = htons(DEFAULT_PORT);//设置的端口为DEFAULT_PORT

//将本地地址绑定到所创建的套接字上
if( bind(socket_fd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1){
printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno);
exit(0);
}
//开始监听是否有客户端连接
if( listen(socket_fd, 10) == -1){
printf("listen socket error: %s(errno: %d)\n",strerror(errno),errno);
exit(0);
}
printf("======waiting for client's request======\n");
while(1){

//阻塞直到有客户端连接,不然多浪费CPU资源。
if( (connect_fd = accept(socket_fd, (struct sockaddr*)NULL, NULL)) == -1){
printf(“accept socket error: %s(errno: %d)”,strerror(errno),errno);
continue;
}
//接受客户端传过来的数据
n = recv(connect_fd, buff, MAXLINE, 0);
//向客户端发送回应数据
if(!fork()){ /紫禁城/
if(send(connect_fd, “Hello,you are connected!\n”, 26,0) == -1)
perror(“send error”);
close(connect_fd);
exit(0);
}
buff[n] = ‘\0’;
printf(“recv msg from client: %s\n”, buff);
close(connect_fd);
}
close(socket_fd);
}




客户端:




/* File Name: client.c */

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

#define MAXLINE 4096

int main(int argc, char** argv)
{
int sockfd, n,rec_len;
char recvline[4096], sendline[4096];
char buf[MAXLINE];
struct sockaddr_in servaddr;

if( argc != 2){
printf("usage: ./client <ipaddress>\n");
exit(0);
}


if( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
printf("create socket error: %s(errno: %d)\n", strerror(errno),errno);
exit(0);
}


memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8000);
if( inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0){
printf("inet_pton error for %s\n",argv[1]);
exit(0);
}


if( connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0){
printf("connect error: %s(errno: %d)\n",strerror(errno),errno);
exit(0);
}


printf("send msg to server: \n");
fgets(sendline, 4096, stdin);
if( send(sockfd, sendline, strlen(sendline), 0) < 0)
{
printf("send msg error: %s(errno: %d)\n", strerror(errno), errno);
exit(0);
}
if((rec_len = recv(sockfd, buf, MAXLINE,0)) == -1) {
   perror("recv error");
   exit(1);
}
buf[rec_len]  = '\0';
printf("Received : %s ",buf);
close(sockfd);
exit(0);

}


inet\_pton 是Linux下IP地址转换函数,可以在将IP地址在“点分十进制”和“整数”之间转换 ,是inet\_addr的扩展。




int inet_pton(int af, const char *src, void *dst);//转换字符串到网络地址:


第一个参数af是地址族,转换后存在dst中  
     af = AF\_INET:src为指向字符型的地址,即ASCII的地址的首地址(ddd.ddd.ddd.ddd格式的),函数将该地址转换为in\_addr的结构体,并复制在\*dst中  
   af =AF\_INET6:src为指向IPV6的地址,函数将该地址转换为in6\_addr的结构体,并复制在\*dst中  
 如果函数出错将返回一个负值,并将errno设置为EAFNOSUPPORT,如果参数af指定的地址族和src格式不对,函数将返回0。



测试:


编译server.c


gcc -o server server.c


启动进程:


./server


显示结果:


======waiting for client's request======


并等待客户端连接。


编译 client.c


gcc -o client client.c


客户端去连接server:


./client 127.0.0.1 


**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化的资料的朋友,可以点击这里获取!](https://bbs.csdn.net/topics/618635766)**

**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值