简介

套接字是操作系统中用于网络通信的重要结构,它是建立在网络体系结构的传输层,用于主机之间数据的发送和接收,像web中使用的http协议便是建立在socket之上的。这一节主要讨论网络套接字。

套接字接口时一组函数,它们和Unix I/O结合起来,用于创建网络应用。许多操作系统都实现了自己的套接字接口。在Unix中,可以将套接字视为一个文件,使用文件I/O函数对套接字进行操作,这也贯彻了Unix中一切皆文件的思想。

既然是网络通信,那么就需要服务端和客户端,一个基本的客户端和服务端的通信模型如下,其中方框里为使用的函数接口:

由于套接字本质上也是一个文件,所以上图中的recvsend也可以使用文件I/O函数readwrite替代。

套接字地址结构

既然要进行网络通信,那么就要有基本的网络的地址。因特网的套接字地址存放在一个叫sockaddr_in的16字节的结构体中,其中的IP地址和端口号都是以网络字节序(大端序)存放的:

struct sockaddr_in {
uint16_t sin_family; /* 协议类型,总是AF_INET */
uint16_t sin_port; /* 端口号 */
struct in_addr sin_addr; /* IP地址 */
unsigned char sin_zero[8]; /* sizeof(struct in_addr) */
}; struct sockaddr {
uint16_t sin_family; /* 协议类型 */
char sa_data[14]; /* 地址信息 */
}

注意到,上面还有一个sockaddr结构体类型。由于connectbindaccept都需要接受一个指向与协议相关的套接字地址结构指针。但是在设计这些接口时,如何定义这些接口,使之能够接受各种类型的套接字地址。为了解决这个问题,设计了sockaddr这种通用的结构体,在使用这些接口时,都需要将与协议相关的结构体转化为这个通用的结构体。

创建和关闭套接字

使用socket函数创建一个套接字,函数原型如下:

#include <sys/types.h>
#include <sys/socket.h> /* 返回:若成功则为非负描述符,失败返回-1 */
int socket(int domain, int type, int protocol);

参数domain(域)用于确定通讯的特性,具体如下:

描述
AF_INET IPv4因特网域
AF_INET6 IPv6因特网域
AF_UNIX UNIX域
AF_UPSPEC 未指定

参数type确定套接字的类型,进一步确定通讯特性:

类型 描述
SOCK_DGRAM 固定长度的,无连接的,不可靠的报文传递
SOCK_RAW IP协议的数据报接口
SOCK_SEQPACKET 固定长度的,有序的,可靠的,面向连接的报文协议
SOCK_STREAM 有序的,可靠的,双向的,面向连接的字节流(TCP)

参数protocol通常是0,表示使用默认协议,其他选项这里不再赘述。

例如可以像下面这样创建一个套接字:

fd = socket(AF_INET, SOCK_STREAM, 0);

其中,AF_INET表示我们使用32位IPv4地址,SOCK_STREAM表示我们使用TCP协议。但是最好的方法使用getaddrinfo函数来自动生成这些函数,这样我们的代码就与具体的协议无关了。我们会在下方进行讲述。

socket函数返回的描述符只是部分打开的,还不能直接使用,取决于是作为客户端还是服务端,需要一些初始化工作。

shutdown可以用来禁止一个套接字的I/O:

#include <sys/socket.h>

/* 返回:若成功返回1,若出错返回-1 */
int shutdown(int sockfd, int how);

参数how指明了关闭套接字的方式:

标志 描述
SHUT_RD 关闭读端
SHUT_WR 关闭写端
SHUT_RDWR 关闭读写

尽管使用close函数也够关闭套接字,但是和shutdown函数却有两点不同:当对一个套接字描述符调用close,该套接字的引用计数会减一,只有当引用计数为0时才会真正关闭套接字,而shutdown不管引用计数是否为0,都会关闭套接字。另外,shutdown可以只关闭套接字读写两个方向中的一个方向,保留另一个方向继续工作。

绑定地址

作为服务器,需要给套接字关联一个众所周知的地址,这样别人才能使用这个地址来对服务器进行访问。

使用bind函数来关联地址和套接字:

#include <sys/socket.h>

/* 返回:若成功,返回0,出错,返回-1 */
int bind(int sockfd, const struct sockaddr *addr, socklen_t len);

bind函数将告诉内核将addr中的服务器套接字地址和套接字sockfd关联起来。其中lensizeof(sockaddr_in)

对于使用的服务器地址有如下限制:

  • 指定的地址必须有效,不能是其他机器的地址
  • 地址必须和创建套接字时的地址族所支持的格式相匹配
  • 地址中的端口号必须不小于1024,除非该进程有相应特权
  • 一般只能将一个套接字端点绑定到一个地址。

建立监听

作为服务器,为了接受用户的连接,需要监听某个特定的端口。当客户请求连接这个端口时,便创建一个连接。

默认情况下,内核会认为socket函数创建的套接字为主动套接字,它存在于用于连接的客户端。调用listen函数来告诉内核描述符是被服务器所使用的,而不是客户端。

#include <sys/socket.h>

/* 返回:成功返回0,失败返回-1 */
int listen(int sockfd, int backlog);

listen函数将一个主动套接字转化为一个监听套接字,该套接字可以接受来自客户端的连接请求。backlog参数指明请求队列中未完成连接的数量。当队列已满,进程将拒绝后续的连接请求。

等待连接

一但服务器调用了listen,所用的套接字就可以用来接受连接请求,使用accept函数来接受客户端的连接请求。

#include <sys/socket.h>

/* 返回:若成功,返回非负连接描述符,失败返回-1 */
int accept(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict len);

accept返回的描述符是一个新的已连接的描述符,这个套接字描述符用来和客户端进行通信。这个新的套接字和原始套接字(sockfd)具有相同的套接字类型和地址族,传送给accept的原始套接字并没有关联到这个连接,继续可用并接受其他连接。

accept函数会将客户端的地址信息填充到参数addr指向的缓冲区,参数len为缓冲区的大小。如果不需要这些信息。可以将addrlen设为NULL。

如果没有连接请求,accept会阻塞到一个连接请求的到来。如果sockfd处于非阻塞模式,则返回-1,并将errno设置为EAGAINEWOULDBLOCK

建立连接

对于客户端来说,如果要处理一个面向连接的网络服务(SOCK_STREAMSOCK_SEQPACKET)。在交换数据之前,首先要在客户端和服务端之间建立一个连接。

使用connect函数来建立一个连接:

#include <sys/socket.h>

/* 返回:成功返回0,出错误返回-1 */
int connect(int sockfd, const struct sockaddr *addr, socklen_t len);

connect函数会试图与套接字地址为addr的服务器建立一个连接,其中lensizeof(sockaddr_in)connect函数会阻塞,直到完成连接的建立或出错。随后,描述符sockfd便可进行读写了。

发送和接收

可以使用send函数来发送数据:

#include <sys/socket.h>

/* 返回:若成功,返回发送的字节数,若出错,返回-1 */
ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags);

send函数与用于文件读写的write函数基本一致,只是多了第四个参数flags,一般置为0。

使用recv来接收数据:

#include <sys/socket.h>

/* 返回:返回数据的字节长度,若无可用数据或对方已经按序结束,返回0,若出错返回-1 */
ssize_t recv(int sockfd, void *buf, size_t nbytes, int flags);

recv函数与read函数基本一致,只是多了第四个参数flags,来管理如何接收数据,一般置为0。

主机和服务的转换

在前面对套接字的创建中,我们采用手动指明协议的方式,但是这样编写并不恰当。Linux提供了一些函数用于主机名和服务名与地址之间的相互映射,使用这些函数可以使我们的程序独立于任何特定版本的IP协议。下面看一下这些函数。

getaddrinfo函数用于将主机名,主机地址,服务名和端口号的字符串表示转化为套接字地址结构。

#include <sys/socket.h>
#include <netdb.h> /* 返回:若成功返回0,若出错返回非零错误码 */
int getaddrinfo(const char *restrict host,
const char *restrict service,
const struct addrinfo *restrict hint,
strcut addrinfo **restrict res); /* 返回:无 */
void freeaddrinfo(struct addrinfo *ai); /* 返回:错误消息字符串 */
const char *gai_strerror(int errcode);

host参数指定主机地址,可以是域名或者点分十进制的IP地址。service参数可以是服务名也可以是十进制的端口号。主机名和服务名可以两者都提供,也可以只提供一个,但另一个必须是一个空指针。

getaddrinfo函数返回一个链表结构addrinfores指向该结构的第一个节点。使用freeaddrinfo函数可以释放该结构。addrinfo结构的定义至少包含以下成员:

struct addrinfo {
int ai_flags; /* customize behavior */
int ai_family; /* address family,first arg to socket function */
int ai_socktype; /* socket type,second arg to socket function */
int ai_protocol; /* protocol,third arg to socket function */
char *ai_canonname; /* canonical name for hostname */
socklen_t ai_addrlen; /* length in byte of address */
struct sockaddr *ai_addr; /* address */
struct addrinfo *ai_next; /* next in list */
...
};

getaddrinfo可以指定一个可选的hint来过滤符合条件的地址,包括ai_familyai_flagsai_protocolai_socktype字段。剩余的字段必须置为0或空指针。

下表总结了ai_flags字段的标志,这些标志可以通过or运算指定多个:

标志 描述
AI_ADDRCONFIG 查询配置的地址类型(IPv4或IPv6)。使用连接时推荐。只有当本地主机被配置为IPv4时,getaddrinfo返回IPv4地址。对IPv6同样
AI_ALL 查找IPv4和IPv6地址(仅用于AI_V4MAPPED)
AI_CANONNAME 需要一个规范的名字(与别名相对)
AI_NUMERICHOST 以数字格式指定主机地址
AI_NUMERICSERV 将服务指定为数字端口号
AI_PASSIVE 套接字地址用于监听绑定
AI_V4MAPPED 如果没有找到IPv6地址,返回映射到IPv6格式的IPv4地址

如果getaddrinfo出错,可以将返回的错误码传入gai_strerror函数获取具体的错误信息。

接下来的getnameinfo函数与getaddrinfo函数功能相反,用于将一个地址转化为一个主机名和一个服务名:

#include <sys/socket.h>
#include <netdb.h> /* 返回:若成功返回0,出错返回非0值 */
int getnameinfo(const struct sockaddr *restrict addr, socklen_t alen,
char *restrict host, socklen_t hostlen,
char *restrict service, socklen_t servlen,
int flags);

getnameinfo将字节长度为alen的套接字地址addr翻译成一个主机名和一个服务名,如果host非空,则指向一个字节长度为hostlen的缓冲区用于存放主机名。同样的,如果service非空,则指向一个长度为servlen的缓冲区来存放服务名。flags参数指定了函数工作的方式,如下:

标志 描述
NI_DGRAM 服务基于数据报而非基于流
NI_NAMEREQD 如果找不到主机名,将其作为一个错误对待
NI_NOFQDN 对于本地主机,仅返回全限定域名的节点名部分
NI_NUMERICHOST 函数默认返回host中的域名,该标志指定返回主机的数字形式,而非主机名
NI_NUMERICSCOPE 对于IPv6,返回范围ID的数字形式,而非名字
NI_NUMERICSERV 函数默认检查/etc/services,如果可能会返回服务名而不是端口号。该标志指定跳过检查,返回服务地址的数字形式(端口号),而非名字

下面一个来自书中的例子展示域名到它IP地址的映射:

/* main.c */
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h> #define MAXLEN 8192 int main(int argc, char **argv) {
struct addrinfo *p, *listp, hints;
char buf[MAXLEN], buf1[MAXLEN];
int rc, flags; if(argc != 2) {
fprintf(stderr, "argv error\n");
exit(-1);
} memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
if((rc = getaddrinfo(argv[1], NULL, &hints, &listp)) != 0) {
fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(rc));
exit(-1);
} flags = NI_NUMERICHOST;
for(p = listp; p; p = p->ai_next) {
getnameinfo(p->ai_addr, p->ai_addrlen, buf, MAXLEN, buf1, MAXLEN, flags);
printf("%s %s\n", buf, buf1);
} freeaddrinfo(listp);
exit(0);
}

使用如下:

$ gcc main.c -o main
$ ./main baidu.com
39.156.69.79 0
220.181.38.148 0

小结

unix提供了一组函数用于创建套接字并完成数据传输:

  • 使用getaddrinfo函数和socket函数来创建套接字,可以使程序更具有通用性。
  • 服务端使用bind给套接字绑定地址,使用listen指定套接字用于监听,并使用accept接受连接
  • 客户端使用connect来连接到服务器
  • 客户端和服务端都可以使用sendrecv来传递数据
  • 使用closeshutdown函数来关闭套接字

总结自《深入理解计算机系统》《Unix环境高级编程》

Unix套接字接口的更多相关文章

  1. 网络IPC:套接字接口概述

    网络IPC:套接字接口概述 套接字接口实现了通过网络连接的不同计算机之间的进程相互通信的机制. 套接字描述符(创建套接字) 套接字是通信端点的抽象,为创建套接字,调用socket函数 #include ...

  2. Linux/UNIX套接字连接

    套接字连接 套接字是一种通信机子.凭借这样的机制.客户/server系统的开发工作既能够在本地单机上进行.也能够夸网络进行. 套接字的创建和使用与管道是有差别的.由于套接字明白地将客户和server区 ...

  3. 【T05】套接字接口比XTI_TLI更好用

    1.用于网络编程的API接口有两种: Berkeley套接字 XTL 2.套接字是加州大学伯克利分校为其Unix操作系统版本开发的,TLI是AT&T(贝尔实验室)为Unix系统V3.0开发的 ...

  4. fsockopen — 打开一个网络连接或者一个Unix套接字连接

    fsockopen (PHP 4, PHP 5, PHP 7) 说明 resource fsockopen ( string $hostname [, int $port = -1 [, int &a ...

  5. 一个基于Unix套接字的注册登录系统

    2016/5/5 今天,我参考<Unix网络编程-卷1>第5章的TCP回射客户/服务器程序写了一个简单的注册登录系统,其功能如下:(1)注册.客户端向服务器发送个人信息请求注册,服务器查询 ...

  6. UNIX网络编程——网络IPC:套接字

    UNIX网络编程——网络IPC:套接字 Contents 套接字接口 套接字描述符 寻址 字节序 地址格式 地址查询 绑定地址 建立连接 数据传输 套接字选项 带外数据 UNIX域套接字 使用套接字的 ...

  7. socket , 套接口还是套接字,傻傻分不清楚

    socket 做网络通信的朋友大都对socket这个词不会感到陌生,但是它的中文翻译是叫套接口还是套接字呢,未必大多数朋友能够分清,今天我们就来聊聊socket的中文名称. socket一词的起源 在 ...

  8. UNIX 网络编程笔记-CH3:套接字编程简介

    IPv4套接字地址结构 struct in_addr { in_addr_t s_addr; }; struct sockaddr_in { uint8_t sin_len; /* length of ...

  9. unix, PF_UNIX, AF_UNIX, PF_LOCAL, AF_LOCAL - 用于本地内部进程通讯的套接字。

    SYNOPSIS(总览) #include <sys/socket.h> #include <sys/un.h> unix_socket = socket(PF_UNIX, t ...

随机推荐

  1. android 获取webview内容真实高度(webview上下可滚动距离)

    正常获取: mainWebView.getContentHeight()//获取html高度 mainWebView.getScale()//手机上网页缩放比例 mainWebView.getHeig ...

  2. [HTML] websocket的模拟日志监控界面

    模拟命令行的界面效果,使用swoole作为websocket的服务,重新做了下html的界面效果 <html> <head> <title>SwLog Montio ...

  3. 圆桌问题 (ArrayList+模拟)

    圆桌上围坐着2n个人.其中n个人是好人,另外n个人是坏人.如果从第一个人开始数数,数到第m个人,则立即处死该人:然后从被处死的人之后开始数数,再将数到的第m个人处死……依此方法不断处死围坐在圆桌上的人 ...

  4. e.printStackTrace()打印在哪里以及如何e.printStackTrace()的内容打印在日志中

    1.e.printStackTrace()打印在哪里 在catch中的e.printStackTrace()将打印到控制台 2.e.printStackTrace()打印的内容是什么 如下代码: im ...

  5. mysql 父子表 注意事项

    今天遇到一个问题,父子表关联查询时总是多出几条数据,后来排查是父子关系的字段 类型不一致导致的

  6. 安装apache ActiveMQ

    笔者环境 操作系统centos 6.9 java jdk 1.8 activemq版本 5.15.9 a)安装activemq需要jdk 环境,这里使用的是 jdk 1.8 yum安装jdk比较简单, ...

  7. C. Polygon for the Angle 几何数学

    C. Polygon for the Angle 几何数学 题意 给出一个度数 ,问可以实现的最小的n的n边形是多少 思路 由n边形的外角和是180度直接就可以算出最小的角是多少 如果给出的度数是其最 ...

  8. 【转载】SpringMVC框架介绍

    转自:http://com-xpp.iteye.com/blog/1604183 SpringMVC框架图   SpringMVC接口解释   DispatcherServlet接口: Spring提 ...

  9. MNIST 数据集

    mnist 数据集:包含 7 万张黑底白字手写数字图片,其中 55000 张为训练集,5000 张为验证集,10000 张为测试集.每张图片大小为 28*28 像素,图片中纯黑色像素值为 0,纯白色像 ...

  10. linx下跑多个tomcat

    1.修改server.xml文件 <Server port="8005" shutdown="SHUTDOWN"> <Connector po ...