0x04基础套接字

套接字基本概念

创建套接字

套接字在Linux系统中表现位文件描述符,形式上由一个int类型定义的整数表示,套接字的创建通过下面的Linux系统调用函数实现:

#include <sys/types.h>
#include <sys/socket.h>
//头文件sys/type.h对于某些C的宏是必须的;sys/socket.h对于定义socket函数是必须的
int socket(int domain, int type, int protocol);
//返回值:文件描述符表示成功,-1表示错误,errno记录错误代码

socket 可以看成是用户进程和内核网络协议栈的编程接口。
socket 不仅可以用于本机的进程间通信,还可以用于网络上不同主机进程间的通信。

参数:

  • 套接字的域名(domain),代表套接字的地址族
  • 套接字的类型(type)
  • 使用的协议(protocol),一般情况下该参数是0,表示由系统当前设定的domain下,自动选择合适的协议

域和地址族

Linux系统支持的domain参数主要有:

域名 地址族
AF_UNIX,AF_LOCAL 用于本地通信
AF_INET,PF_INET IPv4,Internet协议
AF_INET6 IPv6,Internet协议
AF_IPX Novell网络协议
AF_X25 ITU-T X.25/ISO-8208协议

套接字地址

通用套接字地址

#include <sys/socket.h>
struct sockaddr{
  sa_family_t   sa_family;//地址族
  char          sa_data[4];//地址数据
};

一般不会直接使用该结构进行地址设置,它只是作为一个通用类型,以确定所有其他具体地址的结构,即确定所有其他具体地址的结构,即任何具体地址类型都必须具有sa_family成员,因为它决定了怎样翻译结构体中所包含的地址信息。

Internet(IPv4)套接字地址

IPv4套接口地址结构通常也称为“网际套接字地址结构”,它以“sockaddr_in”命名,定义在头文件<netinet/in.h>

#include <netinet/in.h>

struct in_addr{
  uint32_t  s_addr;//IP地址
};

struct sockaddr_in{
  sa_family_t   sin_family;//地址族
  uint16_t      sin_port;//端口
  struct in_addr    sin_addr;//IP地址
  unsigned char     sin_zero[8];//占位字节
};

sockaddr_in结构体中各个成员的描述如下:

  • sin_family,与通用套接字定义中的sa_family相同,初始化我iAF_INET或PF_INET
  • sin_port,位端口号,必须是网络字节序的形式
  • sin_addr,具体定义在struct in_addr结构中,必须是网络字节序的形式,代表IP地址,实际为一个32位无符号整数
  • sin_zero[8],占位字节,使整个结构以16字节的形式对其,它并不被使用,所以不需要初始化

流式套接字和数据报套接字

流式套接字(SOCK_STREAM)

流式套接字类型用于套接字之间进行流式I/O操作。所谓流是指在一对互相连接的套接字的一端写入的字节数据被另一端接收,接收方所收到的字节数据中没有所谓的边界或分界符,也没有所谓的记录长度、块大小或数据分组等概念,只要有数据可读,则数据都将返回给数据接收方的缓存。

流式套接字的另一个特点是数据严格按照写入时的顺序被接收端所读取。

流式套接字提供的是可靠的数据传输,采用面向连接的方式。

数据报套接字(SOCK_DGRAM)

数据报套接字用于无连接通信,即通信前双方不需要建立任何连接,只要创建了一个非链接的数据报套接字,就可以向任何愿意接收信息的套接字发送消息。UDP协议就是典型的数据报通信方式。

无连接通信时传输的数据不需要严格按照发送时的顺序被接收方所接收,甚至不需要严格地进行传输,允许丢失部分数据,当丢失部分数据时,不试图进行重传恢复。

使用套接字

创建套接字函数socket

连接请求函数connect

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

int connect( int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
//返回值:0表示成功,-1表示失败,errno记录错误代码

connect函数试图与套接字地址未addr的服务器建立一个因特网连接,其中addrlen是sizeof(sockaddr_in)。connect函数会阻塞,一直到连接成功建立或是发生错误。如果成功,sockfd描述符现在就准备好可以读写了,并且得到的连接是由套接字对(x:y,servaddr->sin_addr:servaddr->sin_port)刻画得,其中x表示客户端的IP地址,而y表示临时端口,它唯一地确定了客户端主机上的客户端进程。
参数分别是:

  • sockfd,调用socket函数所生成的套接字
  • servaddr,客户端准备连接地服务器地址
  • addrlen,服务器地址长度

对于TCP套接字,执行connect函数后将启动TCP连接地三次握手过程,连接要么成功建立,要么失败。

绑定本地地址函数bind

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

int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
//返回值:0表示成功,-1表示失败,errno记录错误代码

通过socket()函数创建套接字后,该套接字处于未和任何协议地址关联状态,此时虽然仍然可以使用,但是仅限本地一对互联套接字地特殊情况。因此套接字创建后,需执行bind()函数,将套接字绑定到指定的协议地址。

  • sockfd,调用socket函数所生成地套接字
  • myaddr,分配给套接字sockfd的地址指针
  • addrlen,服务器地址长度

    对于TCP套接字,通过bind可以同时指定端口、IP地址,或者仅仅指定端口或IP地址,甚至也可以都不指定,具体如下:

  • TCP服务器在启动时将绑定到众所周知的端口。如果TCP的客户端或服务器端不进行绑定操作,则当应用执行connect或listen后,由内核负责为此套接字选择一个临时端口。通常客户端不显式绑定端口,而服务器端需要明确进行绑定

  • 应用进程可以指定套接字绑定到特定的IP地址。对于TCP客户端,绑定的IP地址将作为该套接字所发出IP报文的源地址。对于TCP服务器,绑定的IP地址将限制套接字只能接收发送到此IP地址的连接请求

通常TCP客户端套接字不显式绑定到某个IP地址上,而是当连接建立时,由内核根据到达服务器的路由来自动选择源地址进行绑定。

IP地址参数和端口值含义:

IP地址 端口 含义
INADDR_ANY 0 内核选择IP地址和端口
INADDR_ANY 非0 内核选择IP,应用确定端口
本地IP 0 应用确定IP,内核选择临时端口
本地IP 非0 应用选择IP和端口

监听函数listen

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

int listen (int sockfd, int backlog);
//返回值:0成功,-1失败,errno记录错误代码

该函数只用于TCP服务器启动监听,有两个输入参数:

  • 用于监听的套接字sockfd
  • 连接队列的长度backlog

backlog参数是指在完成时TCP三次握手之后已经成功建立TCP连接的队列长度,服务器执行accept操作从该队列中取下一个连接进行后续处理,backlog值默认位128.

  • 对于给定的监听套接口,要维护两个队列
    • 1、已由并到达服务器,服务器正在等待完成相应的TCP三次握手过程
    • 2、已完成连接的队列
    • 两队列之和不超过backlog

一旦调用listen函数后,该套接字便成了被动套接字,否则默认为主动套接字。

主动套接字 用于发起连接,会调用connect函数来发起连接
被动套接字 用于接受连接,会调用accept函数来接受连接

backplog队列管理

接收请求函数accept

TCP服务器使用accept函数从backlog队列中返回下一个成功建立的连接,如果backlog队列位空,则服务器进程将被阻塞,进入休眠状态。

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

int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
//返回值:文件描述符表示成功,-1表示失败,errno记录失败代码

accept函数有以下三个参数:

  • sockfd,用于监听的套接字
  • cliaddr,用于接收客户端套接字地址的地址结构指针
  • addrlen,指向接收的套接字地址缓存最大长度的指针。这个指针所指向的整型数作为输入参数,指定cliaddr的最大长度;作为输出参数,代表函数返回时地址的实际长度

如果该函数调用成功,返回值是一个新的套接字描述符,称为连接套接字,服务器使用该套接字和已经建立连接的客户端进行通信,而原有的监听套接字继续接收后续新客户端发来的连接请求。连接套接字在通信完毕后通常立刻被关闭,但是监听套接字将一直处于监听状态直到整个应用结束。

套接字I/O操作

流式套接字可以像文件一样读写和关闭,其方法如下:

#include <unistd.h>

ssize_t read(int sockfd, void *buf, size_t count);
//返回值:非0表示所读字节数,0表示文件尾,-1表示失败,errno记录错误代码

ssize_t write(int sockfd, const void *buf, size_t count);
//返回值:非0表示所写字节数,0表示未写任何数据,-1表示失败,errno记录错误代码

int close(int sockfd);
int shutdown(int sockfd, int how);
//返回值:0表示成功,-1表示失败,errno记录错误代码

read函数有以下3个参数:

  • sockfd,用于读操作的套接字
  • buf,用于存放读入数据的缓存
  • count,代表本次read操作可以接收的最大可读数据字节长度,通常为buf所指向的接收缓存的大小

read函数的返回值代表实际所读数据的字节长度,当函数返回0时表示都到了文件尾即EOF,当未读到数据且没有遇到文件尾时,该函数将被阻塞。

write函数有以下3个参数:

  • sockfd,用于写数据的套接字
  • buf,存放被写数据的缓存
  • count,被写数据字节长度,通常该值为buf所指向的输出缓存的大小

write函数返回值代表实际所写的字节数,该值一般应该等于count,但是在有些情况下二者可能不同。

函数close和shutdown都是关闭套接字。前者是完全关闭,而后者用于希望在完全关闭本地套接字前仍然可以从远端套接字继续接收数据,但不允许本地再发送任何数据。shutdown通常用于即将结束通信的善后处理,其参数how代表如何部分关闭本地套接字:

how 值 说明
0 SHUT_RD 不允许本地socket进行读操作
1 SHUT_WR 不允许本地socket进行写操作
2 SHUT_RDWR 不允许本地socket进行读和写操作(等于close)

数据报套接字的读写方法如下:

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

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);

//返回值:fei0表示成功发送或接收的字节数,-1表示失败,errno记录错误代码

sendto函数用于向指定的接收者的地址发送数据报,它具有如下参数:

  • 用于发送数据报的套接字sockfd
  • 用于存放发送数据的应用缓存指针buf
  • 数据报消息的长度len
  • 发送选项flags,对于普通情况设置为0
  • 接收方套接字地址指针dest_addr
  • 接收地址的长度addrlen

sendto函数调用成功,返回所发送的数据报字节数(注意并不能保证这些发送成功的数据一定会被远端数据报套接字正确接收),当调用发生错误时,返回-1并且errno指出错误原因。

recvfrom函数用于接收数据报数据,它具有以下参数:

  • 用于接收数据报的套接字sockfd
  • 用于存放接收数据所在的应用缓存指针buf
  • 应用缓存的最大长度len
  • 接收选项flags,对于普通情况设置为0
  • src_addr(可选)指针,指向装有源地址的缓冲区,指明从哪里接收数据报
  • addrlen(可选)指针,指向src_addr缓冲区长度值

同样,recvfrom函数返回-1表示出错,并由errno指出错误原因,否则返回buf中实际接收到的字节数。

在连接中EOF意味什么

首先,我们需要理解其实并没有像EOF字符这样的一个东西。进一步来说,EOF是由内核检测到的一种条件。应用程序在它接收到一个由read函数返回的零返回码时,它就会发现出EOF条件。对于磁盘文件,当前文件位置超出文件长度时,会发生EOF。对于网络连接,当一个进程关闭连接它的那一端时,会发生EOF,连接另一端的进程在试图读取流中最后一个字节之后的字节时,会检测到EOF。

套接字编程常用基本函数和数据类型

字节序转换函数

在x86体系下,主机字节序是以最低有效位在前的方式存储,即小端字节序(little-endian),而Internet使用的是网络字节序,最高有效位在前,即大端字节序(big-endian)。

#include <netinet/in.h>
/*将一个主机序无符号长整型数转换为网络字节序无符号长整型数*/
unsigned long htonl(unsigned long host_long);
/*将一个主机序16位无符号整数转换为网络字节序16位无符号整数*/
unsigned short htons(unsigned short host_short);

/*将一个网络字节序无符号长整型数转换为主机序无符号长整型数*/
unsigned long ntohl(unsigned long net_long);
/*将一个网络字节序16位无符号整数转换为主机序16位无符号整数*/
unsigned short ntohs(unsigned short net_short);

地址转换函数

//下面两个函数分别用于点分十进制表示的字符串IP地址和网络字节序表示的32比特地址整数之间进行转换

#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>

/*将字符串形式的IP地址转换为网络字节序32比特整型IP地址*/
int inet_aton(const char *cp, struct in_addr *inp);
//返回值:非0表示IP字符串合法,0表示非法

/*将网络字节序32比特整型IP地址转换为字符串IP形式*/
char *inet_ntoa(struct in_addr in);

//converts the Internet host address cp from IPv4 numbers-and-dots notation into binary data in network byte order
in_addr_t inet_addr(const char *cp);

//converts cp, a string in IPv4 numbers-and-dots notation, into  a  number  in host  byte  order suitable for use as an Internet network address.
in_addr_t inet_network(const char *cp);

//returns an Internet host address in network byte order, created by combining the network number net with the local address host, both in host byte order.
struct in_addr inet_makeaddr(in_addr_t net, in_addr_t host);

//returns the local network address  part  of  the  Internet  address  in. 
in_addr_t inet_lnaof(struct in_addr in);

//returns  the  network number part of the Internet address in. 
in_addr_t inet_netof(struct in_addr in);

inet_aton具有如下参数:

  • cp,以点分十进制表示的IP地址字符串
  • inp,指针指向以网络字节序表示的IP地址结构体

inet_aton函数的返回值存放在一个静态分配的缓存中,因此随后调用该函数将导致之前的返回值被覆盖。

主机信息函数

#include <netdb.h>
struct hostent *gethostbyname(const char *name);
//返回值:若成功则返回hostent结构体指针,返回NULL表示错误,h_errno记录错误代码

struct hostent{
  char *h_name;/*主机的正式名称*/
  char **h_aliases;/*主机的别名列表*/
  int h_addrtype;/*主机地址类型*/
  int h_length;/*地址长度*/
  char **h_addr_list;/*地址列表,每个IP地址都实际上是以网络序表示的32比特整型数*/

};
/*保持向后兼容*/
#define h_addr h_addr_list[0]

gethostbyname函数返回的指针指向一个静态分配的缓存,随后再次调用该函数将覆盖之前的返回值。

客户端/服务器通信方式

流式套接字客户端/服务器通信过程

流式套接字客户端_服务器通信

服务器端过程:

  1. 服务器首先通过socket()系统调用创建监听套接字;
  2. 之后调用bind()将套接字绑定到指定地址和端口;
  3. 接着服务器调用listen()进行监听,等待客户端发送来请求。
  4. 当客户端通过connect()发出TCP连接建立请求后,服务器TCP就与客户端TCP之间通过三次握手建立连接。连接建立后,客户端的连接请求就被放入服务器的backlog队列中。
  5. 服务器应用通过执行accept(),从backlog队列中取出下一个已经成功建立的连接进行处理。如果此backlog队列为空,则服务器将被阻塞,直到队列中出现成功建立连接的请求为止。
  6. 服务器应用对服务器连接套接字执行read()操作,获得客户端发来的请求信息。
  7. 服务器应用处理完客户端请求后,通过对服务器连接套接字执行write()操作,将处理结果发回给客户端。
  8. 当服务器与客户端完成通信后,双方都需要调用close()关闭各自的连接套接字。

客户端过程:

  1. 客户端首先调用socket()创建用于连接的套接字;
  2. 之后调用connect(),向指定地址和端口的服务器发起TCP连接。(客户端不需要显示执行bind(),因为通常是由客户端主动发起连接请求,因此客户端套接字会在发出connect()调用后,由内核自动绑定到一个临时端口和地址上。而作为服务器一般工作在被动连接方式下,所有必须通过显示调用bind(),将监听套接字绑定到一个众所周知的端口上,以等待客户端的连接。)
  3. 客户端通过connect()建立TCP连接后,执行write()操作将请求信息写入客户端连接套接字;
  4. 当服务器与客户端完成通信后,双方都需要调用close()关闭各自的连接套接字。

数据报套接字客户端/服务器通信过程

数据报套接字客户端_服务器通信

服务器端过程:

  1. 数据报套接字服务器首先通过socket()系统调用创建监听套接字;
  2. 接着调用bind()将监听套接字绑定到指定的地址和端口;
  3. 然后服务器执行recvfrom(),等待客户端法力UDP数据报。

客户端过程:

  1. 客户端调用socket()创建用于UDP数据报发送的套接字;
  2. 之后调用sendto()向指定地址和端口的数据报套接字服务器发送数据。(客户端并不需要显示执行bind().)。

小结

套接字是Linux操作系统为用户态应用提供的网络编程接口,应用程序通过套接字实现网络通信。

posted @ 2018-04-10 15:44  main_c  阅读(191)  评论(0编辑  收藏  举报