Linux下的socket编程

套接字(socket)

1.什么是套接字

  套接字(socket)是一种通信机制,凭借这种机制,客户/服务器系统的开发工作既可以在本地单机上进行,也可以跨网络进行。Linux所提供的功能(如打印服务、连接数据库和提供Web页面)和网络工具(如用于远程登录的rlogin和用于文件传输的ftp)通常都是通过套接字来进行通信的。

2.套接字连接

  你可以把套接字连接想象为打电话进一个繁忙的办公大楼。一个电话打到一家公司,接线员接听电话并把它转到正确的部门(服务器进程),然后再从那里转到电话要找的人(服务器套接字)。每个进入的电话呼叫(客户)都被转到正确的终端节点,而中间介入的接线员则可以空出来处理后续的电话。在开始学习Linux系统中的套接字连接是如何建立之前,我们需要先理解套接字应用程序是如何通过套接字来维持一个连接的。

  首先,服务器应用程序用系统调用socket来创建一个套接字,它是系统分配给该服务器进程的类似文件描述符的资源,它不能与其他进程共享。

  接下来,服务器进程会给套接字起个名字。本地套接字的名字是Linux文件系统中的文件名,一般放在/tmp或/usr/tmp目录中。对于网络套接字,它的名字是与客户连接的特定网络有关的服务标识符(端口号或访问点)。这个标识符允许Linux将进入的针对特定端口号的连接转到正确的服务器进程。例如,Web服务器一般在80端口上创建一个套接字,这是一个专用于此目的的标识符。Web浏览器知道对于用户想要访问的Web站点,应该使用端口80来建立HTTP连接。我们用系统调用bind来给套接字命名。然后服务器进程就开始等待客户连接到这个命名套接字。系统调用listen的作用是,创建一个队列并将其用于存放来自客户的进入连接。服务器通过系统调用accept来接受客户的连接。

  服务器调用accept时,它会创建一个与原有的命名套接字不同的新套接字。这个新套接字只用于与这个特定的客户进行通信,而命名套接字则被保留下来继续处理来自其他客户的连接。如果服务器编写得当,它就可以充分利用多个连接带来的好处。Web服务器就会这么做以同时服务来自许多客户的页面请求。对一个简单的服务器来说,后续的客户将在监听队列中等待,直到服务器再次准备就绪。

  基于套接字系统的客户端更加简单。客户首先调用socket创建一个未命名套接字,然后将服务器的命名套接字作为一个地址来调用connect与服务器建立连接。一旦连接建立,我们就可以像使用底层的文件描述符那样用套接字来实现双向的数据通信。

2.1创建一个套接字:socket()

socket系统调用创建一个套接字并返回一个描述符,该描述符可以用来访问该套接字。

  #include <sys/types.h>

  #include <sys/socket.h>

  int socket(int domain, int type, int protoco1);

  创建的套接字是一条通信线路的一个端点。domain参数指定协议族,type参数指定这个套接字的通信类型,protocol参数指定使用的协议。

  domain可选参数AF_INET、AF_INET6。AF_INET表示IPv4的socket,AF_INET6就表示IPv6的socket。

  type参数指定了socket类型。可选参数SOCK_STREAM(流格式套接字/面向连接的套接字)和 SOCK_DGRAM(数据报套接字/无连接的套接字)

  protocol参数一般socket类型中总会被指定为0。当protocol为0时,会自动选择type类型对应的默认协议。一般也常用IPPROTO_TCP 和 IPPTOTO_UDP,分别表示 TCP 传输协议和 UDP 传输协议。

  socket()在成功时返回一个引用在后续系统调用中会用到的新创建的socket 的文件描述符

2.2套接字地址结构:struct sockaddr/ struct sockaddr_in/ struct sockaddr_un

  struct sockaddr:是一种通用的结构体,可以用来保存多种类型的IP地址和端口号,这个类型的唯一用途是将各种domain特定的地址结构转换成单个类型以供socket系统调用中的各个参数使用。

  sockaddr_in:主要门用来保存 IPv4 地址的用于不同主机之间的socket编程。

  sockaddr_un:主要用于同一个主机中的本地socket用以进程间通信的一种方式。

 1 struct sockaddr{  
 2      sa_family_t    sin_family;        //地址族
 3     char            sa_data[14];    //14字节,包含套接字中的目标地址和端口信息               
 4    }; 
 5 
 6 struct sockaddr_in{
 7     sa_family_t        sin_family;       //地址族(Address Family),也就是地址类型
 8     uint16_t        sin_port;         //16位的端口号
 9     struct in_addr  sin_addr;        //32位IP地址
10     char            sin_zero[8];      //不使用,一般用0填充
11 };
12 struct sockaddr_un 
13 {
14 
15   sa_family_t    sun_family;             //AF_UNIX
16   char            sun_path[UNIX_PATH_MAX]; //路径名
17   };

 

2.3将socket绑定到地址:bind()

  #include <sys/socket.h>

  int bind(int sockfd, struct sockaddr *addr, socklen_t addrlen);

  sockfd参数是在上一个socket()调用中获得的文件描述符。addr参数是一个指针,它指向了一个指定该socket绑定到的地址的结构。addrlen参数指定了地址结构的大小。  

  一般来讲,会将一个服务器的 socket绑定到一个众所周知的地址——即一个固定的与服务器进行通信的客户端应用程序提前就知道的地址,即初始化addr:

  addr.sin_family = AF_INET; //使用IPv4地址

  addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址

  addr.sin_port = htons(1234); //端口

  bind调用成功时返回0,失败时返回-1并设置errno为相应的值。

2.4创建套接字队列:listen()

  #include <sys/socket.h>

  int listen(int sockfd, int backlog);

  为了能够在套接字上接受进入的连接,服务器程序必须创建一个队列来保存未处理的请求。它用listen系统调用来完成这一工作。

  sockfd:为需要进入监听状态的套接字。

  backlog:为请求队列的最大长度。Linux系统可能会对队列中可以容纳的未处理连接的最大数目做出限制。为了遵守这个最大值限制,listen函数将队列长度设置为backlog参数的值。在套接字队列中,等待处理的进入连接的个数最多不能超过这个数字。再往后的连接将被拒绝,导致客户的连接请求失败。listen函数提供的这种机制允许当服务器程序正忙于处理前一个客户请求的时候,将后续的客户连接放入队列等待处理。在 Linux 上,这个常量的值被定义成了128(可以修改)。

  listen调用成功时返回0,失败时返回-1并设置errno为相应的值。

2.5接受连接:accept()

  #include <sys/socket.h>

  int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

  一旦服务器程序创建并绑定了套接字之后,它就可以通过accept系统调用来等待客户建立对该套接字的连接。accept() 返回一个新的套接字来和客户端通信,addr 保存了客户端的IP地址和端口号,而 sock 是服务器端的套接字,后面和客户端通信时,使用新生成的套接字,而不是原来服务器端的套接字。监听socket (sockfd)会保持打开状态,并且可以被用来接受后续的连接。

  如果套接字队列中没有未处理的连接,accept将阻塞(程序将暂停)直到有客户建立连接为止。

2.6连接到对等的socket:connect()

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

  addr和 addrlen参数的指定方式与bind()调用中对应参数的指定方式相同。

  如果连接不能立刻建立,connect调用将阻塞一段不确定的超时时间。一旦这个超时时间到达连接将被放弃,connect调用失败。

2.7关闭套接字:close()

  int close(int sockfd);

  sockfd:要关闭的套接字描述符

  终止一个流 socket连接的常见方式是调用close()。如果多个文件描述符引用了同一个socket,那么当所有描述符被关闭之后连接就会终止。注意与的shutdown()区别

总结

  一个典型的流 socket 服务器会使用socket()创建其 socket,然后使用bind()将这个 socket绑定到一个众所周知(即需要通信的ip地址+端口)的地址上。服务器接着调用listen()以允许在该socket上接受连接。监听socket 上的客户端连接是通过accept()来接受的,它将返回一个与客户端的socket进行连接的新socket的文件描述符。

  一个典型的流socket客户端会使用socket()创建一个socket,然后通过调用connect()建立一个连接并制定服务器的众所周知的地址。当两个流socket连接之后就可以使用read()和 write()在任意一个方向上传输数据了。一旦拥有引用一个流 socket端点的文件描述符的所有进程都执行了一个隐式或显示的close()之后,连接就会终止。

参考资料

Linux程序设计+中文第4版

Linux_UNIX系统编程手册

posted @ 2023-04-13 03:22  图南本南  阅读(126)  评论(0编辑  收藏  举报