套接字在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 不仅可以用于本机的进程间通信,还可以用于网络上不同主机进程间的通信。
参数:
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成员,因为它决定了怎样翻译结构体中所包含的地址信息。
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结构体中各个成员的描述如下:
流式套接字类型用于套接字之间进行流式I/O操作。所谓流是指在一对互相连接的套接字的一端写入的字节数据被另一端接收,接收方所收到的字节数据中没有所谓的边界或分界符,也没有所谓的记录长度、块大小或数据分组等概念,只要有数据可读,则数据都将返回给数据接收方的缓存。
流式套接字的另一个特点是数据严格按照写入时的顺序被接收端所读取。
流式套接字提供的是可靠的数据传输,采用面向连接的方式。
数据报套接字用于无连接通信,即通信前双方不需要建立任何连接,只要创建了一个非链接的数据报套接字,就可以向任何愿意接收信息的套接字发送消息。UDP协议就是典型的数据报通信方式。
无连接通信时传输的数据不需要严格按照发送时的顺序被接收方所接收,甚至不需要严格地进行传输,允许丢失部分数据,当丢失部分数据时,不试图进行重传恢复。
#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表示临时端口,它唯一地确定了客户端主机上的客户端进程。
参数分别是:
对于TCP套接字,执行connect函数后将启动TCP连接地三次握手过程,连接要么成功建立,要么失败。
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
//返回值:0表示成功,-1表示失败,errno记录错误代码
通过socket()函数创建套接字后,该套接字处于未和任何协议地址关联状态,此时虽然仍然可以使用,但是仅限本地一对互联套接字地特殊情况。因此套接字创建后,需执行bind()函数,将套接字绑定到指定的协议地址。
对于TCP套接字,通过bind可以同时指定端口、IP地址,或者仅仅指定端口或IP地址,甚至也可以都不指定,具体如下:
通常TCP客户端套接字不显式绑定到某个IP地址上,而是当连接建立时,由内核根据到达服务器的路由来自动选择源地址进行绑定。
IP地址参数和端口值含义:
IP地址 | 端口 | 含义 |
---|---|---|
INADDR_ANY | 0 | 内核选择IP地址和端口 |
INADDR_ANY | 非0 | 内核选择IP,应用确定端口 |
本地IP | 0 | 应用确定IP,内核选择临时端口 |
本地IP | 非0 | 应用选择IP和端口 |
#include <sys/types.h>
#include <sys/socket.h>
int listen (int sockfd, int backlog);
//返回值:0成功,-1失败,errno记录错误代码
该函数只用于TCP服务器启动监听,有两个输入参数:
backlog参数是指在完成时TCP三次握手之后已经成功建立TCP连接的队列长度,服务器执行accept操作从该队列中取下一个连接进行后续处理,backlog值默认位128.
一旦调用listen函数后,该套接字便成了被动套接字,否则默认为主动套接字。
主动套接字 | 用于发起连接,会调用connect函数来发起连接 |
---|---|
被动套接字 | 用于接受连接,会调用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函数有以下三个参数:
如果该函数调用成功,返回值是一个新的套接字描述符,称为连接套接字,服务器使用该套接字和已经建立连接的客户端进行通信,而原有的监听套接字继续接收后续新客户端发来的连接请求。连接套接字在通信完毕后通常立刻被关闭,但是监听套接字将一直处于监听状态直到整个应用结束。
流式套接字可以像文件一样读写和关闭,其方法如下:
#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个参数:
read函数的返回值代表实际所读数据的字节长度,当函数返回0时表示都到了文件尾即EOF,当未读到数据且没有遇到文件尾时,该函数将被阻塞。
write函数有以下3个参数:
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函数用于向指定的接收者的地址发送数据报,它具有如下参数:
sendto函数调用成功,返回所发送的数据报字节数(注意并不能保证这些发送成功的数据一定会被远端数据报套接字正确接收),当调用发生错误时,返回-1并且errno指出错误原因。
recvfrom函数用于接收数据报数据,它具有以下参数:
同样,recvfrom函数返回-1表示出错,并由errno指出错误原因,否则返回buf中实际接收到的字节数。
首先,我们需要理解其实并没有像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具有如下参数:
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函数返回的指针指向一个静态分配的缓存,随后再次调用该函数将覆盖之前的返回值。
服务器端过程:
客户端过程:
服务器端过程:
客户端过程:
套接字是Linux操作系统为用户态应用提供的网络编程接口,应用程序通过套接字实现网络通信。