什么是socket
socket是一种操作系统提供的进程间通信机制。
在操作系统中,通常会为应用程序提供一组应用程序接口(API),称为套接字接口(socket API)。应用程序可以通过套接字接口,来使用网络套接字,以进行数据交换。
在套接字接口中,以IP地址及通信端口组成套接字地址。远程的套接字地址,以及本地的套接字地址完成连接后,再加上使用的协议,这个五元组,作为套接字对,之后就可以彼此交换数据。
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
说白了Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
这些接口的实现都是内核来完成。
0是标准输入,1是标准输出,2是标准错误输出。
网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。
在TCP/IP网络应用中,通信的两个进程间相互作用的主要模式是客户/服务器(Client/Server, C/S)模式,即客户向服务器发出服务请求,服务器接收到请求后,提供相应的服务。
(1)客户与服务器进程的作用是非对称的,因此代码不同。
(2)服务器进程一般是先启动的。只要系统运行,该服务进程一直存在,直到正常或强迫终止。
服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。
在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。
客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。
创建套接字
int socket(int domain, int type, int protocol);//返回sockfd
domain 用于指定创建套接字所使用的协议族
协议族决定了socket的地址类型,在通信中必须采用对应的地址
AF_INET(IPV4 协议)
AF_INET6(IPV6协议)
AF_UNIX 只在本机内通信的套接字
type:指定socket类型。
SOCK_STREAM:创建TCP流套接字。提供面向连接的稳定数据传输
SOCK_DGRAM:创建UDP数据报套接字。使用不连续不可靠的数据包连接。
SOCK_RAW:创建原始套接字。提供原始网络协议存取。
SOCK_PACKET:与网络驱动程序直接通信。
SOCK_SEQPACKET:提供连续可靠的数据包连接。
SOCK_RDM: 提供可靠的数据包连接
protocol:故名思意,就是指定协议。通常设为0,通过参数domain指定的协议族和参数type指定套接字类型来确定参数。当创建原始套接字,系统无法惟一的协议,这时候就要使用该参数来指定协议。
建立连接
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与服务器的连接。
绑定套接字
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。
addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。
addrlen:对应的是地址的长度。
通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。
在套接字上监听
int listen(int sockfd, int backlog);
listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。
接受连接
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数sockfd就是监听套接字,这个套接字用来监听一个端口,当有一个客户与服务器连接时,它使用这个一个端口号,而此时这个端口号正与这个套接字关联。当然客户不知道套接字这些细节,它只知道一个地址和一个端口号。
这是一个结果参数,它用来接受一个返回值,这返回值指定客户端的地址,当然这个地址是通过某个地址结构来描述的,用户应该知道这一个什么样的地址结构。如果对客户的地址不感兴趣,那么可以把这个值设置为NULL。
如同大家所认为的,它也是结果的参数,用来接受上述addr的结构的大小的,它指明addr结构所占有的字节个数。同样的,它也可以被设置为NULL。
如果accept成功返回,则服务器与客户已经正确建立连接了,此时服务器通过accept返回的套接字来完成与客户的通信。
accept默认会阻塞进程,直到有一个客户连接建立后返回,它返回的是一个新可用的套接字,这个套接字是连接套接字。
此时我们需要区分两种套接字,
监听套接字: 监听套接字正如accept的参数sockfd,它是监听套接字,在调用listen函数之后,是服务器开始调用socket()函数生成的,称为监听socket描述字(监听套接字)
连接套接字:一个套接字会从主动连接的套接字变身为一个监听套接字;而accept函数返回的是已连接socket描述字(一个连接套接字),它代表着一个网络已经存在的点点连接。
一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。
TCP套接字的数据传输
发送
ssize_t send(int s , const void *msg , size_t len , int flags);
函数send只能对处于连接状态的套接字使用,参数s为已建立好连接的套接字描述符,即accept的返回值。参数msg指向存放待发送数据的缓冲区,参数len为待发送数据的长度。
flags为控制选项,一般设为0或取以下值。
MSG_OOB:在指定套接字上发送外带数据,该类型的套接字必须支持外带数据。
MSG_DONTROUTE:通过最直接的路径发送数据,而忽略下层协议的路由设置。
发送的数据太长而不能发送时,将出现错误,errno为EMSGSIZE;如果要发送的数据长度大于该套接字的缓冲区剩余空间大小时,send一般会被阻塞,如果该套接字为非阻塞方式,则此时立即返回-1并将errno设为EAGAIN
执行成功返回实际发送数据的字节数,出错则返回-1
注意 :执行成功只是说明数据写入套接字的缓冲区中,并不表示数据已经通过网络发送到目的地。
接收
ssize_t recv(int s , void *buf , size_t len , int flags);
从参数s所指定的套接字描述符(必须是面向连接的套接字)上接收数据并保存到参数buf所指定的缓冲区,参数len则为缓冲区的长度。
参数flags为控制选项,一般设置为0或取以下值:
MSG_OOB:请求接受外带数据
MSG_PEEK:只查看数据而不读出
MSG_WAITALL:只在接受缓冲区满时才返回
如果一个数据包太长以至于缓冲区无法放下时,剩余部分的数据有可能被丢弃(根据套接字的类型)。如果在指定的套接字上无数据到达时,recv被阻塞,如果该套接字为非阻塞方式,则立即返回-1并将errno设置为EAGAIN。recv接受到数据就返回,不会等到接受到参数len指定的长度才返回。
执行成功返回实际接收数据的字节数,出错则返回-1
UDP套接字的数据传输
发送
ssize_t sendto(int s, const void *msg,size_t len,int flags,const struct sockaddr *to,socklen_t tolen);
和send差不多,sendto不需要套接字处于连接状态,常用来发送UDP数据。要指定数据目的地址和目的地址的长度。
接收
ssize_t recvfrom(int s, void *buf, size_t len,int flags,struct sockaddr *from, socklen_t *fromlen);
用来接收UDP数据,需要指定源地址和源地址长度。
关闭
int close(int fd);
关闭一个套接字描述符,该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。
int shutdown(int s,int how);
与close类似,但是功能更强大,可以对套接字的关闭进行一些更细致的控制,它允许对套接字进行单向关闭或者全部禁止。
SHUT_RD:读通道关闭,进程将不能再接收任何数据,接受缓冲区还未被读取的数据也将被丢弃,但仍然可以在该套接字上发送数据。
SHUT_WR:写通道关闭,进程将不能再发送任何数据,发送缓冲区还未被发送的数据也将被丢弃,但仍然可以在该套接字上接收数据。
SHUT_RDWR:读、写通道都被关闭
主机字节序与网络字节序
主机字节序就是我们平常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机字节序。
a) Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
b) Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
网络字节序:4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序的问题了。
所以:在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是Big-Endian。所以务必将其转化为网络字节序再赋给socket。
uint32_t htonl(uint32_t hostlong);//将主机unsigned int型数据转换成网络字节顺序
uint16_t htons(uint16_t hostshort);//将主机unsigned short型数据转换成网络字节顺序
uint32_t ntohl(uint32_t netlong);//与htonl相反
uint16_t ntohs(uint16_t netshort);//与htons相反
inet系列函数
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp);
将cp所指向的字符串形式的IP地址转换为二进制的网络字节序的IP地址,执行成功返回非零值,参数无效返回0
in_addr_t inet_addr(const char *cp);
跟inet_aton类似,执行成功将结果返回,参数无效返回INADDR_NONE,一般为-1,可能使“255.255.255.255”成为无效地址
in_addr_t inet_network(const char *cp);
将字符串形式的网络地址转换为主机字节顺序形式的二进制IP地址,成功返回转换后的结果,参数无效返回-1
char *inet_ntoa(struct in_addr in);
将值为in的网络字节顺序形式的二进制IP地址转换成以”.”分隔的字符串形式,执行成功返回结果字符串指针,参数无效返回NULL
struct in_addr inet_makeaddr(in_addr_t net, in_addr_t host);
把网络号为参数net,主机号为参数host的两个地址组合一个网络地址
in_addr_t inet_lnaof(struct in_addr in);
从参数in提取出主机地址,执行成功返回主机字节顺序形式的主机地址
in_addr_t inet_netof(struct in_addr in);
从参数in提取出网络地址,执行成功返回主机字节顺序形式的网络地址
getsockopt与setsockopt
#include <sys/types.h>
#include <sys/socket.h>
int getsockopt(int sock, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int sock, int level, int optname, const void *optval, socklen_t optlen);
参数:
sock:将要被设置或者获取选项的套接字。
level:选项所在的协议层。可以取SOL_SOCKET(通用套接字),IPPROTO_IP(IP层套接字),IPPROTO_TCP(TCP层套接字)等值,一般取SOL_SOCKET来进行与特定协议不相关的操作。
optname:需要访问的选项名。
optval:对于getsockopt(),指向返回选项值的缓冲。对于setsockopt(),指向包含新选项值的缓冲。
optlen:对于getsockopt(),作为入口参数时,选项值的最大长度。作为出口参数时,选项值的实际长度。对于setsockopt(),现选项的长度。
成功执行时,返回0。失败返回-1,errno被设为以下的某个值
EBADF:sock不是有效的文件描述词
EFAULT:optval指向的内存并非有效的进程空间
EINVAL:在调用setsockopt()时,optlen无效
ENOPROTOOPT:指定的协议层不能识别选项
ENOTSOCK:sock描述的不是套接字
选项名称 说明 数据类型
SOL_SOCKET
SO_BROADCAST 允许发送广播数据 int
SO_DEBUG 允许调试 int
SO_DONTROUTE 不查找路由 int
SO_ERROR 获得套接字错误 int
SO_KEEPALIVE 保持连接 int
SO_LINGER 延迟关闭连接 struct linger
SO_OOBINLINE 带外数据放入正常数据流 int
SO_RCVBUF 接收缓冲区大小 int
SO_SNDBUF 发送缓冲区大小 int
SO_RCVLOWAT 接收缓冲区下限 int
SO_SNDLOWAT 发送缓冲区下限 int
SO_RCVTIMEO 接收超时 struct timeval
SO_SNDTIMEO 发送超时 struct timeval
SO_REUSERADDR 允许重用本地地址和端口 int
SO_TYPE 获得套接字类型 int
SO_BSDCOMPAT 与BSD系统兼容 int
IPPROTO_IP
IP_HDRINCL 在数据包中包含IP首部 int
IP_OPTINOS IP首部选项 int
IP_TOS 服务类型
IP_TTL 生存时间 int
IPPRO_TCP
TCP_MAXSEG TCP最大数据段的大小 int
TCP_NODELAY 不使用Nagle算法 int
返回说明:
成功执行时,返回0。失败返回-1,errno被设为以下的某个值
EBADF:sock不是有效的文件描述词
EFAULT:optval指向的内存并非有效的进程空间
EINVAL:在调用setsockopt()时,optlen无效
ENOPROTOOPT:指定的协议层不能识别选项
ENOTSOCK:sock描述的不是套接字