一、socket简介
socket,即套接字是一种通信机制,凭借这种机制,客户/服务器(即要进行通信的进程)系统的开发工作既可以在本地单机上进行,也可以跨网络进行。
也就是说它可以让不在同一台计算机但通过网络连接计算机上的进程进行通信。也因为这样,套接字明确地将客户端和服务器区分开来。
二、套接字的属性
套接字的特性由3个属性确定,它们分别是:域、类型和协议。
1、套接字的域
它指定套接字通信中使用的网络介质,最常见的套接字域是AF_INET,它指的是Internet网络。
当客户使用套接字进行跨网络的连接时,它就需要用到服务器计算机的IP地址和端口来指定一台联网机器上的某个特定服务,
所以在使用socket作为通信的终点,服务器应用程序必须在开始通信之前绑定一个端口,服务器在指定的端口等待客户的连接。
另一个域AF_UNIX表示UNIX文件系统,它就是文件输入/输出,而它的地址就是文件名。
2、套接字类型
因特网提供了两种通信机制:流(stream)和数据报(datagram),因而套接字的类型也就分为流套接字和数据报套接字。这里主要讲流套接字。
流套接字由类型SOCK_STREAM指定,它们是在AF_INET域中通过TCP/IP连接实现,同时也是AF_UNIX中常用的套接字类型。
流套接字提供的是一个有序、可靠、双向字节流的连接,因此发送的数据可以确保不会丢失、重复或乱序到达,而且它还有一定的出错后重新发送的机制。
与流套接字相对的是由类型SOCK_DGRAM指定的数据报套接字,它不需要建立连接和维持一个连接,它们在AF_INET中通常是通过UDP/IP协议实现的。
它对可以发送的数据的长度有限制,数据报作为一个单独的网络消息被传输,它可能会丢失、复制或错乱到达,
UDP不是一个可靠的协议,但是它的速度比较高,因为它并一需要总是要建立和维持一个连接。
3、套接字协议
只要底层的传输机制允许不止一个协议来提供要求的套接字类型,我们就可以为套接字选择一个特定的协议。通常只需要使用默认值。
三、套接字地址
每个套接字都有其自己的地址格式,对于AF_UNIX域套接字(本地通信)来说,它的地址由结构sockaddr_un来描述,该结构定义在头文件sys/un.h中,它的定义如下:
struct sockaddr_un{ sa_family_t sun_family;//AF_UNIX,它是一个短整型 char sum_path[];//路径名 };
对于AF_INET域套接字(不同主机通信)来说,它的地址结构由sockaddr_in来描述,它至少包括以下几个成员:
struct sockaddr_in{ short int sin_family;//AF_INET unsigned short int sin_port;//端口号 struct in_addr sin_addr;//IP地址 };
上面我们已经知道网络中的进程是通过socket来通信的,那什么是socket呢?
socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。
我的理解就是Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)。
四、套接字函数
1、socket()函数
int socket(int domain, int type, int protocol);
socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。
这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。
正如可以给fopen的传入不同参数值,以打开不同的文件。创建socket的时候,也可以指定不同的参数创建不同的socket描述符,
socket函数的三个参数分别为:
• domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INET、AF_INET6、AF_UNIX(Unix域socket)、AF_ROUTE等等。
协议族决定了socket的地址类型,在通信中必须采用对应的地址,如
AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
AF_UNIX/PF_UNIX/AF_LOCAL/PF_LOCAL:Unix进程通信协议(本地通信); AF_INET/PF_INET:IPv4网络协议;(不同主机通信) AF_PACKET/PF_PACKET:初级封包接口;(自定义数据包)
• type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等。
1).流套接字(SOCK_STREAM)
流套接字用于提供面向连接、可靠的数据传输服务。该服务将保证数据能够实现无差错、无重复送,并按顺序接收。
流套接字之所以能够实现可靠的数据服务,原因在于其使用了传输控制协议,即TCP(The Transmission Control Protocol)协议 。
2).数据报套接字(SOCK_DGRAM)
数据报套接字提供一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。
数据报套接字使用UDP( User DatagramProtocol)协议进行数据的传输。
由于数据报套接字不能保证数据传输的可靠性,对于有可能出现的数据丢失情况,需要在程序中做相应的处理 。
3).原始套接字(SOCK_RAW)
原始套接字与标准套接字的区别在于:原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,
数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送的数据必须使用原始套接 。
• protocol:故名思意,就是指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,
它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。
注意:网络字节序与主机字节序
主机字节序就是我们平常说的大端和小端模式:不同的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。
常用字节转化函数:
u_long htonl(u_long hostlong):将unsigned long类型主机字节序转换为TCP/IP网络字节序. u_short htons(u_short hostshort):将unsigned short类型主机字节序转换为TCP/IP网络字节序. unsigned long in inet_addr(const char *cp):将一个点分制的IP地址(如192.168.0.1)转换为网络二进制数字 int inet_aton(cont char* cp, struct in_addr *inp):将网络地址转为网络二进制数字,与inet_addr的区别是,结果不是作为返回值,而是保存形参inp所指的in_addr结构体中 char *inet_ntoa(struct in_addr in):将网络二进制数字转为网络地址
在使用little endian的系统中 这些函数会把字节序进行转换。
在使用big endian类型的系统中 这些函数会定义成空宏。
同样,在网络程序开发时 或是跨平台开发时,也应该注意保证只用一种字节序。
注意:并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。
当protocol为0时,会自动选择type类型对应的默认协议。
当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。
如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口。
2、bind()函数
正如上面所说bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
函数的三个参数分别为:
• sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。
• addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,如
ipv4对应的是: struct sockaddr_in { sa_family_t sin_family; in_port_t sin_port; struct in_addr sin_addr; }; struct in_addr { uint32_t s_addr; }; ipv6对应的是: struct sockaddr_in6 { sa_family_t sin6_family; in_port_t sin6_port; uint32_t sin6_flowinfo; struct in6_addr sin6_addr; uint32_t sin6_scope_id; }; struct in6_addr { unsigned char s6_addr[16]; }; Unix域对应的是: #define UNIX_PATH_MAX 108 struct sockaddr_un { sa_family_t sun_family; char sun_path[UNIX_PATH_MAX]; };
• addrlen:对应的是地址的长度。
通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;
而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。
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、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);
accept函数的第一个参数为服务器的socket描述字,第二个参数为指向struct sockaddr *的指针,用于返回客户端的协议地址,
第三个参数为协议地址的长度。如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。
注意:accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;
而accept函数返回的是已连接的socket描述字。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。
内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。
5、read()、write()等函数
万事具备只欠东风,至此服务器与客户已经建立好连接了。可以调用网络I/O进行读写操作了,即实现了网咯中不同进程之间的通信!网络I/O操作有下面几组:
• read()/write()
• recv()/send()
• readv()/writev()
• recvmsg()/sendmsg()
• recvfrom()/sendto()
我推荐使用recvmsg()/sendmsg()函数,这两个函数是最通用的I/O函数,实际上可以把上面的其它函数都替换成这两个函数。它们的声明如下:
#include ssize_t read(int fd, void *buf, size_t count); ssize_t write(int fd, const void *buf, size_t count); #include #include 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。
6、close()函数
在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。
int close(int fd);
close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。
该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。
注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。
五、通信流程
1、服务器端
首先服务器应用程序用系统调用socket来创建一个套接安,它是系统分配给该服务器进程的类似文件描述符的资源,它不能与其他的进程共享。
接下来,服务器进程会给套接字起个名字,我们使用系统调用bind来给套接字命名。然后服务器进程就开始等待客户连接到这个套接字。
然后,系统调用listen来创建一个队列并将其用于存放来自客户的进入连接。
最后,服务器通过系统调用accept来接受客户的连接。它会创建一个与原有的命名套接不同的新套接字,
这个套接字只用于与这个特定客户端进行通信,而命名套接字(即原先的套接字)则被保留下来继续处理来自其他客户的连接。
2、客户端
基于socket的客户端比服务器端简单,同样,客户应用程序首先调用socket来创建一个未命名的套接字,
然后将服务器的命名套接字作为一个地址来调用connect与服务器建立连接。
一旦连接建立,我们就可以像使用底层的文件描述符那样用套接字来实现双向数据的通信。
六、同主机通信举例
1、服务器的listen模块
#include <stddef.h> #include <sys/socket.h> #include <sys/un.h> #include <errno.h> #define QLEN 10 / * * Create a server endpoint of a connection. * Returns fd if all OK, <0 on error. */ int serv_listen(const char *name) { int fd, len, err, rval; struct sockaddr_un un; /* create a UNIX domain stream socket */ if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) return(-1); unlink(name); /* in case it already exists */ /* fill in socket address structure */ memset(&un, 0, sizeof(un)); un.sun_family = AF_UNIX; strcpy(un.sun_path, name); len = offsetof(struct sockaddr_un, sun_path) + strlen(name); /* bind the name to the descriptor */ if (bind(fd, (struct sockaddr *)&un, len) < 0) { rval = -2; goto errout; } if (listen(fd, QLEN) < 0) //不阻塞 { /* tell kernel we're a server */ rval = -3; goto errout; } return(fd); errout: err = errno; close(fd); errno = err; return(rval); }
2、服务器accept模块
#include <stddef.h> #include <sys/stat.h> #include <sys/socket.h> #include <sys/un.h> #include <errno.h> int serv_accept(int listenfd, uid_t *uidptr) { int clifd, len, err, rval; time_t staletime; struct sockaddr_un un; struct stat statbuf; len = sizeof(un); if ((clifd = accept(listenfd, (struct sockaddr *)&un, &len)) < 0) return(-1); /* often errno=EINTR, if signal caught */ /* obtain the client's uid from its calling address */ len -= offsetof(struct sockaddr_un, sun_path); /* len of pathname */ un.sun_path[len] = 0; /* null terminate */ if (stat(un.sun_path, &statbuf) < 0) { rval = -2; goto errout; } if (S_ISSOCK(statbuf.st_mode) == 0) { rval = -3; /* not a socket */ goto errout; } if (uidptr != NULL) *uidptr = statbuf.st_uid; /* return uid of caller */ unlink(un.sun_path); /* we're done with pathname now */ return(clifd); errout: err = errno; close(clifd); errno = err; return(rval); }
3、客户端
#include <stdio.h> #include <stddef.h> #include <sys/stat.h> #include <sys/socket.h> #include <sys/un.h> #include <errno.h> #define CLI_PATH "/var/tmp/" /* +5 for pid = 14 chars */ /* * Create a client endpoint and connect to a server. * Returns fd if all OK, <0 on error. */ int cli_conn(const char *name) { int fd, len, err, rval; struct sockaddr_un un; /* create a UNIX domain stream socket */ if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) return(-1); /* fill socket address structure with our address */ memset(&un, 0, sizeof(un)); un.sun_family = AF_UNIX; sprintf(un.sun_path, "%s%05d", CLI_PATH, getpid()); len = offsetof(struct sockaddr_un, sun_path) + strlen(un.sun_path); unlink(un.sun_path); /* in case it already exists */ if (bind(fd, (struct sockaddr *)&un, len) < 0) { rval = -2; goto errout; } /* fill socket address structure with server's address */ memset(&un, 0, sizeof(un)); un.sun_family = AF_UNIX; strcpy(un.sun_path, name); len = offsetof(struct sockaddr_un, sun_path) + strlen(name); if (connect(fd, (struct sockaddr *)&un, len) < 0) { rval = -4; goto errout; } return(fd); errout: err = errno; close(fd); errno = err; return(rval); }
七、不同主机通信举例
1、tcp服务端
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <netinet/in.h> #include <errno.h> #define MAX_MSG_SIZE 256 #define SERVER_PORT 9987 #define BACKLOG 2 int GetServerAddr(char * addrname){ printf("please input server addr:"); scanf("%s",addrname); return 1; } int main(){ int sock_fd,client_fd; /*sock_fd:监听socket;client_fd:数据传输socket */ struct sockaddr_in ser_addr; /* 本机地址信息 */ struct sockaddr_in cli_addr; /* 客户端地址信息 */ char msg[MAX_MSG_SIZE];/* 缓冲区*/ int ser_sockfd=socket(AF_INET,SOCK_STREAM,0);/*创建连接的SOCKET */ if(ser_sockfd<0) {/*创建失败 */ fprintf(stderr,"socker Error:%s ",strerror(errno)); exit(1); } /* 初始化服务器地址*/ socklen_t addrlen=sizeof(struct sockaddr_in); bzero(&ser_addr,addrlen); ser_addr.sin_family=AF_INET; ser_addr.sin_addr.s_addr=htonl(INADDR_ANY); ser_addr.sin_port=htons(SERVER_PORT); if(bind(ser_sockfd,(struct sockaddr*)&ser_addr,sizeof(struct sockaddr_in))<0){ /*绑定失败 */ fprintf(stderr,"Bind Error:%s ",strerror(errno)); exit(1); } /*侦听客户端请求*/ if(listen(ser_sockfd,BACKLOG)<0){ fprintf(stderr,"Listen Error:%s ",strerror(errno)); close(ser_sockfd); exit(1); } while(1){/* 等待接收客户连接请求*/ int cli_sockfd=accept(ser_sockfd,(struct sockaddr*) &cli_addr, &addrlen); if(cli_sockfd<=0){ fprintf(stderr,"Accept Error:%s ",strerror(errno)); }else{/*开始服务*/ recv(cli_sockfd, msg, (size_t)MAX_MSG_SIZE, 0); /* 接受数据*/ printf("received a connection from %sn", inet_ntoa(cli_addr.sin_addr)); printf("%s ",msg);/*在屏幕上打印出来 */ strcpy(msg,"hi,I am server!"); send(cli_sockfd, msg, sizeof(msg),0); /*发送的数据*/ close(cli_sockfd); } } close(ser_sockfd); return 0; }
2、tcp客户端
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <netinet/in.h> #include <errno.h> #define MAX_MSG_SIZE 256 #define SERVER_PORT 9987 int GetServerAddr(char * addrname){ printf("please input server addr:"); scanf("%s",addrname); return 1; } int main(){ int cli_sockfd;/*客户端SOCKET */ int addrlen; char seraddr[14]; struct sockaddr_in ser_addr,/* 服务器的地址*/ cli_addr;/* 客户端的地址*/ char msg[MAX_MSG_SIZE];/* 缓冲区*/ GetServerAddr(seraddr); cli_sockfd=socket(AF_INET,SOCK_STREAM,0);/*创建连接的SOCKET */ if(cli_sockfd<0){/*创建失败 */ fprintf(stderr,"socker Error:%s ",strerror(errno)); exit(1); } /* 初始化客户端地址*/ addrlen=sizeof(struct sockaddr_in); bzero(&ser_addr,addrlen); cli_addr.sin_family=AF_INET; cli_addr.sin_addr.s_addr=htonl(INADDR_ANY); cli_addr.sin_port=0; if(bind(cli_sockfd,(struct sockaddr*)&cli_addr,addrlen)<0){ /*绑定失败 */ fprintf(stderr,"Bind Error:%s ",strerror(errno)); exit(1); } /* 初始化服务器地址*/ addrlen=sizeof(struct sockaddr_in); bzero(&ser_addr,addrlen); ser_addr.sin_family=AF_INET; ser_addr.sin_addr.s_addr=inet_addr(seraddr); ser_addr.sin_port=htons(SERVER_PORT); if(connect(cli_sockfd,(struct sockaddr*)&ser_addr, addrlen)!=0)/*请求连接*/ { /*连接失败 */ fprintf(stderr,"Connect Error:%s ",strerror(errno)); close(cli_sockfd); exit(1); } strcpy(msg,"hi,I am client!"); send(cli_sockfd, msg, sizeof(msg),0);/*发送数据*/ recv(cli_sockfd, msg, MAX_MSG_SIZE,0); /* 接受数据*/ printf("%s ",msg);/*在屏幕上打印出来 */ close(cli_sockfd); return 0; }
参考:
https://blog.csdn.net/pashanhu6402/article/details/96428887
https://blog.csdn.net/panker2008/article/details/46502783
https://baike.baidu.com/item/%E5%A5%97%E6%8E%A5%E5%AD%97/9637606?fromtitle=socket&fromid=281150&fr=aladdin
https://www.jianshu.com/p/066d99da7cbd
https://www.cnblogs.com/huqian23456/archive/2011/02/22/1961822.html
https://blog.csdn.net/will130/article/details/53326740
https://blog.51cto.com/mingtangduyao/1721604
https://blog.csdn.net/giantpoplar/article/details/47657303
https://blog.csdn.net/ljianhui/article/details/10477427#
https://www.cnblogs.com/dapaitou2006/p/6502195.html
https://blog.csdn.net/weixin_43206704/article/details/89327572
https://blog.csdn.net/weixin_43206704/article/details/89293187
http://blog.chinaunix.net/uid-27074062-id-3388166.html
https://blog.csdn.net/aebdm757009/article/details/101498118
https://tennysonsky.blog.csdn.net/article/details/44676377
https://blog.csdn.net/q623702748/article/details/52063019
https://blog.csdn.net/weixin_43288201/article/details/106266418