Linux编程设计——套接字
Posted 张三和李四的家
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux编程设计——套接字相关的知识,希望对你有一定的参考价值。
套接字
套接字,另外一种进程间通信的方式。之前的IPC机制只能限定在一台计算机系统上进行资源共享。而套接字接口可以使,一台机器上的进程和另外一个机器上的进程通信。
什么是套接字
套接字是一种通信机制,凭借这种机制,客户/服务器系统的工作即可以在本地单机上工作,也可以跨网络进行。
套接字和管道类型,同样是读写类文件描述符的操作。不同的是,套接字明确的将客户和服务器分开来。套接字机制可以实现多个客户连接一个服务器。
套接字连接
首先,服务器应用程序使用socket来创建一个套接字,它是系统分配给该服务器进程的类似文件标识符的资源,它不能与其它进程共享。ps:线程貌似也不行。
接下来,服务器进程会给套接字起个名字。本地套接字的名字是Linux文件系统中的文件名。对于网络套接字,它的名字是与客户连接的特定网络有关的服务标识符(端口号或访问点)。这个标识符将允许Linux将进入针对特定端口号的连接转接到正确的服务器进程。例如:Web服务器一般在80端口上创建有关套接字,这是一个专用于此目的的标识符。Web浏览器知道对于用户想访问的Web节点,应该使用端口80来建立HTTP连接。
我们用系统调用bind来给套接字命名(关联于本地某个文件)然后服务器进程就开始等待客户连接这个命名套接字。系统调用listen的作用是,创建一个队列并将其用于存放来自客户的请求连接。服务器通过系统调用accept来接受客户的连接。服务器调用accept时,它会创建一个与原来的命名套接字不同的新套接字。这个套接字只用来与这个特定客户进行通信,而命名套接字则被保留下来继续处理来自其它客户的连接。
基于套接字系统的客户端更加简单。客户首先调用socket创建一个未命名套接字,然后将服务器的命名套接字作为一个地址(或标识符)来调用connect与服务器建立连接。
一旦连接建立,我们就可以想使用底层的文件描述符那样用套接字来实现双向的数据通信。
套接字属性
套接字的特性有3个属性确定,它们是:域(domain)、类型(type)和协议(protocol)
- 套接字的域
域指定套接字通信中使用的网络介质。
- 最常见的套接字域是AF_INET,它指的是Internet网络。许多Linux局域网使用的都是此网络。
- 还有一个域是UNIX文件系统域AF_UNIX,即使一台还为联网的计算机上的套接字也可以使用这个域。这个域的底层协议就是文件输入/输出,而它的地址就是文件名。当运行这个程序时,就可以在当前目录下看到这个地址。
- 服务器计算机上可能有多个服务器正在运行。客户可以通过IP端口来指定一台机器上某个特定服务。在系统内部,端口通过分配一个唯一的16位的整数来标识,在系统外部,则需要通过IP地址和端口号的组合来确定。套接字作为通信的终点,它必须在开始通信之前绑定一个端口。知名的服务通常也有一些端口,比如ftp(21)和httpd(80)等。在选择端口时不要随意选择以避免端口被占用的情况。一般情况下,小于1024的端口号都是为系统服务保留的。
套接字类型
一个套接字域可能有多种不同的通信方式,而每种通信方式又有其不同的特性。但AF_UNIX域的套接字没有这样的问题,它提供了一个可靠的双向通信路径。在网络域中,我们就需要注意底层网络的特性,以及不同的通信机制是如何受到它们的影响。
因特网提供了两种通信机制:流(stream)和数据包(datagram)。他们有着截然不同的服务层次。流套接字
流套接字(在某些方面类似于标准输入/输出流)提供的是一个有序、可靠、双向字节流的连接。ps:使用TCP中的序号机制保证大的消息将被分片、传输、再重组。这很像一个文件流,它接受大量的数据,然后以小数据块的形式将它们写入底层磁盘。
流套接字由底层SOCK_STREAM指定,它们是在AF_INET域中通过TCP/IP连接实现。TCP/IP代表的是传输控制协议(Transmission Control Protocol)/网际协议(Internet Protocol)。IP协议是针对数据包的底层协议,它提供一台计算机通过网络到达另一台计算机的路由。TCP协议提供排序、流控和重传,以保证大数据的传输可以完整地到达目的地或报告一个适当的错误条件。
数据报套接字
与流套接字相反,由类型SOCK_DGRAM指定的数据报套接字不建立和维持一个连接。它对可以发送的数据报的长度有限制。数据报作为一个单独的网络消息被传输,它可能丢失、复制或无序到达。
数据报套接字在AF_INET域中通过UDP/IP连接实现,它提供的是一种无序的不可靠服务。但从资源的角度来看,相对来说它们开销比较小,因此不需要维护网络连接。而且不需要花费时间来建立连接,所以它们的速度也很快。
数据包适用于信息服务中的“单次(single-shot)”查询,它主要用来提供日常状态信息或执行低优先级的日志记录。服务器的奔溃不会给客户造成不便,也不会要求客户重启。
- 套接字协议
暂时使用默认值。
创建套接字
socket系统调用创建一个套接字并返回一个描述符,该描述符可以用来访问该套接字。
int socket(int domin, int type, int protocol);
创建的套接字是一条通信线路的一个端点。domin指定协议族,type指定这个套接字的通信类型,protocol指定使用的协议。
- 参数dome的取值包括:AF_UNIX和AF_INET前者用于UNIX和Linux文件系统实现本地套接字,后者用于UNIX网络套接字,通过包括因特网在内的TCP/IP网络进行通信的程序。
- 参数type取值包括:SOCK_STREAM和SOCK_DGRAM
- protocol:通常不需要选择,将该参数设为0表示使用默认协议。
套接字地址
每个套接字域都有其之间的地址格式。
在AF_UNIX域中,套接字的地址由结构sockaddr_un来表示,该结构定义在头文件sys/un.h中。
struct sockaddr_un { sa_family_t sun_family; /* AF_INET*/ char sun_path[]; /*pathname*/ };
在AF_UNIX域中,套接字的地址由
char sun_path[]
指定。在AF_INET域中,套接字的地址由sockadd_in来指定。该结构定义在netinet/in.h中,它至少包括以下几个成员:
struct sockaddr_in { short int sin_family; /*AF_INET*/ unsigned short int sin_prot; /*Port number*/ struct in_addr sin_addr; /*Internet address*/ }; struct in_addr { unsigned long int s_addr; };
命名套接字
要想让socket创建的套接字可以被其它进程使用,服务器程序就必须给该套接字命名,即将套接字关联到一个文件系统的路径名。
int bind(int sockfd,const struct sockaddr * address,siz_t address_len);
bind系统调用address的地址值与文件描述符socket的未命名套接字相关联。地址结构体的长度由address_len传递。传入参数时需要将一个特定的地址结构体指针(struct sockaddr_in/un*)转换为执行通用地址类型(struct sockaddr*)。成功返回0,失败返回-1并设置errno
创建套接字队列
为了接受多个套接字(客户端)的连接,服务器程序必须创建一个队列来保存未处理的请求。它用listen系统调用来完成这一操作。
int listen(int socket,int backlog);
arg1:命名套接字,arg2:队列的最大长度,即等待处理进入的客户端个数,超过则导致客户端请求失败。与bind返回相同,成功返回0,失败返回-1并设置errno
接受连接
accept系统调用来等待客户建立对该套接字的连接。
int accept(int socket, struct sockaddr * address, size_t *address_len);
accept系统调用只有当客户端试图连接到由socket参数指定的套接字上时才返回。这里的客户指,在套接字队列中排在第一个的未处理连接。
套接字必须先由bind调用关联一个文件系统的路径名(即为套接字命名),然后由listen调用为其分配一个连接队列。连接客户的地址将被放到address参数指向的sockaddr结构中。如果不关心客户端的地址,则将其值设为NULL即可。
参数address_len指定客户结构的长度。如果客户地址的长度超过这个值,它将被截断。
如果套接字队列中没有未处理的连接。accept将堵塞直到有客户建立连接为止。我们可以通过对套接字描述符设置O_NONBLOCK标志来改变这一行为,使用函数fcntl。
int flags = fcntl(socket,F_GETFL,0);
fcntl(socket,F_SETFL,O_NONBLOCK | flags)
当有未处理的客户连接时,accept函数将返回一个新的套接字描述符。发送错误时,返回-1并设置errno
请求连接
客户程序通过一个未命名的套接字和服务器监听套接字之间建立连接的方法来连接到服务器。使用connect调用
int connect(int sockfd, const struct sockaddr *address, size_t address_len);
参数socket指定的套接字将连接到参数address指定的服务器套接字,address指向的结构的长度有参数address_len指定。参数sockfd指定的套接字必须通过socket调用获得一个有效的文件描述符。成功返回0,失败返回-1,设置errno。
如果连接不能立刻建立,connect调用将被堵塞一段不确定的超时时间。一旦超时时间到达,连接将被放弃,connect调用失败。但如果connect调用被一个信号中断,而该信号又得到了处理,connect还会失败。
关闭套接字
使用close函数来终止服务器和客户上的套接字连接,就如同对底层文件描述符进行关闭一样。你应该总是在连接的两端都关闭套接字。对于服务器来说,应该在read调用返回为0即没有数据可读的情况下关闭套接字,但如果有一个套接字是一个面向连接类型的,并且设置了SOCK_LINGER选项,close调用会在该套接字 还有未传输数据时堵塞。此举,需要设置套接字选项。
套接字通信
文件系统套接字的缺点是,除非程序员使用一个绝对路径名,否则套接字将创建在服务器的工作目录下。为了接受客户的连接,你需要创建一个服务器及客户都可访问的全局目录(如:/tmp目录)。而对网络套接字来说,你只需要选择一个未被使用的端口号即可。其它端口号及通过他们提供的服务通常都列在系统文件/etc/services中,在选择端口号,注意不好选择在该配置文件中的端口号。ps:其实也不用看,没有那么好的运气能碰到,只要选择端口号在3000以上,应该就没事。(╯▽╰)
出于演示的目的,我们将使用这个回路网路(一个只包含它自身的回路(loopback)网路)。回路网络对于调试网络应用程序很有用处,因为它排除了任何外部网络问题。回路网路中只包含一台计算机,传统上它被称为localhost,标准地址:127.0.0.1。
每一个与计算机进行通信的网路都有一个与之关联的硬件接口。一台计算机可能在每个网络中都有一个不同的网路名。当然也就会有几个不同的IP地址。
主机字节序和网络字节序
在Linux机器上运行新版本的服务器和客户端时,我们可以用netstat命令来查看网路连接情况。
$ netstat -A inet
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 localhost:9741 localhost:51246 TIME_WAIT
现在可以看到这条对应的服务器和客户的端口号。Local Address一栏显示的服务器的地址和端口号,Foreign Address一栏显示的远程客户的地址和端口号。
可是,当你在程序中选择的端口为3366时,在命令中输出的却是9741.为什么不同呢?答案是:通过套接字传递的端口号和地址都是二进制数字。不同的计算机使用不同的字节序来表示整数。比如:Inter处理器采用小端字节序和网络传输采用的大端字节序。大端字节序意思为数据的地址随之内存的地址变大而变大。
为了使不同类型的计算机可以通过网络传输的多个字节整数的值达成一致,你需要定义一个网络字节序。客户和服务器程序必须在传输前,将它们的内部整数表示方式转化为网络字节序。通过以下函数实现:
unsigned long int htonl(unsigned long int hostlong);
unsigned short int htons(unsigned short int hostshort);
unsigned long int ntohl(unsigned long int netlong);
unsigned short int ntohs(unsigned short int netshort);
这些函数将16位和32位整数在主机字节序和标准的网络字节序之间进行转换。函数名是与之对应的转换操作的简写形式。如果计算机本身的主机字节序和网络字节序相同,这些操作的实际上就是空操作。
转换之后的netstat操作。
$ netstat -A inet
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 localhost:3366 localhost:34411 TIME_WAIT
为了让不同体系结构的计算机上的客户和服务器可以正确的操作,总是在网络程序中使用这些转换函数是很有必要的。
网络信息
到目前为止,我们的客户和服务器程序一直是把地址和端口号编译到程序的内部。对于一个更通用的服务器和客户程序来说,我们可以通过网络信息函数来决定应该使用的地址和端口。
如果你有足够的权限,可以将自己的服务添加到/etc/services文件中的已知服务列表中,并在这个文件中为端口号分配一个名字,使用户可以使用符号化的服务名而不是端口号的数字。
类似的,如果给定一个计算机的名字,你可以通过调用解析地址的主机数据库函数来确定它的IP地址。这些函数通过查询网络配置文件来完成这一工作,如/ect/hosts文件或网络信息服务。常用的网络信息服务有NIS(Network Information Service,网络信息服务,以前叫Yellow Pages,黄页服务)和DNS(Domain Name Service,域名服务)
主机数据库函数在接口头文件netdb.h中声明。
struct hostent *gethostbyaddr(const void *addr, size_t len, int type);
struct hosten *gethostbyname(const char *name);
这些函数返回的数据结构中至少会包含以下几个成员:
struct hostent {
char *h_name; /* official name of host */
char **h_aliases; /* alias list */
int h_addrtype; /* host address type */
int h_length; /* length of address */
char **h_addr_list; /* list of addresses */
}
如果没有我们查询的主机或地址有关的数据项,这些信息函数将返回一个空指针。
类似的,与服务及其关联端口号有关的信息也可以通过一些服务信息函数来获取。
struct servent *getservbyname(const char *name,const char *proto);
struct servent *getservbyport(int port,const char *proto);
proto参数指定用于连接服务的协议,它有两个取值tcp
和udp
,前者用于SOCK_STREAM类型的TCP连接,后者用于SOCK_DGRAM类型的UDP数据报。
返回的数据结构中至少会包含以下几个成员:
struct servent {
char * s_name; /* name of the service*/
char ** s_aliases; /* list of aliases*/
int s_port; /* The IP port number*/
char *s_proto; /* The service type, usually "tcp" or "udp" */
};
如果你想获得某台计算机上的主机数据库信息,可以调用那个gehostbyname函数并且将结果打印出来。注意,要将返回的地址列表信息转化为正确的IP地址类型,需用函数int_ntoa将它们从网络字节序转成主机字节序的字符串后打印出来。
char *inet_ntoa(struct in_addr in);
这个函数的作用是,将有关因特网主机地址转化为一个点分四元组格式的字符串。失败时返回-1,但POSIX规范未定义错误类型。
最后一个函数:gethostname,得到主机的名称
int gethostname(char *name, int legth);
这个函数的作用,将当前主机的主机的名字写入name指向的字符串中。主机名将以NULL结尾。参数length指定了字符串name的长度,如果返回的主机名太长,它就会被截断。调用成功返回0,失败返回-1,适当的设置errno
编写一个连接到标准服务的程序:使用gethostbyname
得到主机的IP地址,getservbyname
得到服务的端口,最后使用connect请求服务。
多客户
目标:如何让单个服务器进程在不堵塞、不等待客户请求到达的前提下处理多个客户。
select系统调用
在编写Linux应用程序时,我们经常会遇到需要检查好几个输入的状态才能确定下一步行动的情况。如果是在一个单用户系统中,运行一个“忙等待”循环还是可以接受的,它不停地扫描输入设备看是否有数据,如果有数据到达就读取它。但这种做法很消耗CPU的时间。
select系统调用允许程序同时在多个底层文件描述符上等待输入的到达(或输出的完成)。这意味着程序可以一直堵塞到有事情可做为止。类似的,服务器也可以通过同时在多个打开的套接字上等待请求到来的方法来处理多个客户。
select函数对数据结构fd_set进行操作,它是由打开的文件描述符而构成的集合。有一组定义好的宏来控制这个集合。
void FD_ZERO(fd_set *fdset);
void FD_CLR(int fd, fd_set *fdset);
void FD_SET(int fd, fd_set *fdset);
int FD_ISSET(int fd, fd_set *fdset);
顾名思义,FD_ZERO用于将fd_set初始化为空集合,FD_SET,FD_CLR就是添加和删除由fd传入到集合中文件描述符。如果FD_ISSET宏中参数fd属于fd_set中一个元素,FD_ISSET宏将返回非零值。fd_set结构中可以容纳的文件描述符的最大数目有常量FD_SETSIZE指定。
select函数通过一个超时值来防止无限期的堵塞。这个超时值由一个timeval结构给出。这个结构定义在头文件sys/time.h中,它由以下几个成员组成:
struct timeval {
time_t tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
类型time_t在头文件sys/types.h中被定义一个整数类型。
select系统调用的原型为:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *except, struct timeval *timeout);
select调用用于测试文件描述符集合中,是否有一个文件描述符已处于可读,可写或其它状态,之后它将堵塞以等待某个文件描述符进入上述状态。
参数nfds指定需要测试的文件描述符的数目,测试的描述符范围从0到nfds-1。3个文件描述符都设为NULL,表示不执行对应测试。
select函数会在发生以下情况时返回:readfds可读,writefds可写,exceptfds相应(不知道什么时候会出现以下情况,旧版中定义为出错时。)如果这三种情况都没有发送,函数将在timeval指定的一段超时时间后返回,如果timeval参数是一个空而且套接字上也没有进入那三种状态,调用将一直堵塞下去。
当select返回时,描述符集合将被修改以指示哪些描述符正处于可读、可写或其它状态。然后使用FD_ISSET对描述符进行测试,来找出需要处理的描述符。如果select是因为超时而返回的,所有的描述符集合将被清空。
select调用成功将返回发生变化的描述符总数,出错返回-1并设置errno。
多客户
服务器可以让select调用同时检查监听套接字和客户的连接套接字。一旦select调用指示有活动发生,就可以使用FD_ISSET来遍历,所有连接的文件描述符,以检查是哪个套接字上面活动发生。
如果是套接字可读的话,这说明正有一个客户请求连接,即服务器使用socket函数创建的套接字描述符有活动,此时就可以调用accept函数接受客户端的连接,如果是某个客户套接字描述符活跃,一个客户需要服务端进行读写操作。如果读操作返回0则表明有一个客户进程已经结束,你可以关闭套接字并把它从描述符集合中删除。
数据包
在有些情况下,在程序中花费时间来建立和维持一个套接字连接是不必须要的。
当用户需要一个短小的数据查询并期望接受到一个短小的相应时,我们一般就使用UDP提供的服务。例如主机上的daytime服务。
因为UDP提供的是不可靠服务,所以你可以会发现数据包或响应会丢失。如果数据包对于你来说非常重要,就需要小心编写UDP程序,以检查错误并在必要是重传。
使用UDP数据包是,你需要使用sendto和recvfrom来代替原有使用在套接字上的read和write调用。
sendto系统调用从buffer缓冲区中给使用指定套接字地址的目标服务器发送一个数据包。
int sendto(int sockfd, void *buffer, size_t len, int flags, struct sockaddr *to, socklen tolen);
在正常调用中,flags参数一般被设置为0.
recvfrom系统调用在套接字上等待从特定地址到来的数据包,并将它放入buffer缓冲区。
int recvfrom(int sockfd, void *buffer, size_t len, int flag, struct sockaddr *to, socklen fromlen);
在正常调用中,flags参数一般被设置为0.
两个函数的调用,成功返回操作的字符数,错误返回-1,并设置errno
通过以上函数可以创建出一个UDP服务器,一个使用sendto函数发数据,一个使用recvfrom函数接数据。另外通过使用setsocketopt
函数来设置套接字描述符的超时情况。也可以采用使用sigaction
信号访问的方式来实现。
以上是关于Linux编程设计——套接字的主要内容,如果未能解决你的问题,请参考以下文章