————总结过往,勉励未来。
抛砖一下:什么是socket?我们平时用socket干嘛?
今天先从网络套接字socket的基本概述讲起,紧接着用一个简单的例子(简易版聊天室)来熟悉socket的一系列函数的应用。
首先, socket是什么?
socket本意是“插座,孔”,scoket作为一种进程间的通信机制,也常被我们称之为“套接字”,用于描述IP地址和端口,从而实现不同主机之间的通信,顾名思义,也就是用来衔接两个东西的一种机制。socket亦被称之为应用层与传输层之间的桥梁。我们常说“Linux下一切皆文件”,正如我们访问文件时可以用文件描述符访问一样,应用程序用套接字描述符来访问套接字。所以,我们先从套接字的结构体入手~
#include <linux/socket.h>
struct sockaddr{
unsigned short sa_family; /*address family*/
char sa_data[14]; /*variable-length address*/
};
其中结构体struct sockaddr定义了一种通用的套接字地址,sa_family表示套接字的协议族类型,对应的TCP/IP协议该值为AF_INET,sa_data存储具体的协议地址。这个结构体但是一般不使用它,二是使用struct sockaddr_in结构体对TCP/IP协议族地址格式进行操作,具体如下:
#include <netinet/in.h>
struct sockaddr_in{
unsigned short sin_family; /*地址类型*/
uint16_t sin_port; /*端口号*/
struct in_addr sin_addr; /*IP地址*/ struct in_addr{uint32_t s_addr;};
unsigned char sin_zero[8]; /*填充字段,一般赋值为0*/
};
在实际应用的时候,上面的结构体被用来设置地址信息,通过设置IP和端口号以及其他信息使得两台主机之间可以进行通信。
因为考虑到不同主机之间的通信时可能会出现大小端的问题,所以就需要一些方法来处理处理器字节序和网络字节序之间的相互转换,具体如下:
#include <arpa/inet.h> h表示“主机”字节序,n表示“网络”字节序,l表示“长整数”, s表示“短整数”
uint32_t htonl(uint32_t hostint32 );
uint16_t htons( uint16_t hostint16 );
uint32_t ntohl( uint32_t netint32 );
uint16_t ntohs( uint16_t netint16 );
当然还有网络字节序本身的二进制与字符串格式之间的转换:
#include <arpa/inet.h>
int inet_pton( int domain, const char *restrict str, void *restrict addr ); //将文本字符串格式转换成网络字节序的二进制地址
const char *inet_ntop( int domain, const void*restrict addr, char *restrict str, socklen_t size ); //将网络字节序的二进制地址转换成文本字符串格式
而我们一般编程时使用最多的就是前者了,这个不用说大家都知道为什么吧>_<
-------------------------------------------------华丽丽华丽丽分割线---------------------------------------------------------------
慢慢来,看完了上面的结构体以及一些常用的转换函数,这个时候就是应该看看一系列的系统调用啦,很实用的函数们。。。记得man哦
我们常说“linux下一切皆文件”,那么对socket也是如此,它也就是可读可写可控制可关闭的文件描述符,我们对它的操作也就可以当作是对于一个文件的操作来看。
No.1 创建socket
#include <sys/types.h>
#include <sys/socket.h>
int socket( int domain, int type, int protocol ); //系统调用成功返回一个socket文件描述符,失败则返回-1,并设置errno
socket函数就相当于是普通文件的打开操作,一般的open一个文件就会返回一个文件描述符,但是socket()函数则是创建一个socket描述符,然后我们就可以利用这个sockfd进行绑定和监听及后续的操作了~
关于参数问题,其中:
domain参数表示使用的底层协议族类型,对于TCP/IP协议族而言一般是设置为PF_INIT;
type参数指定服务类型,对于TCP/IP协议族而言,其值取SOCK_STREAM;若使用SOCK_DGRAM则表示传输层使用UDP协议;当然还有SOCK_RAW、SOCK_SEQPACKET
protocol参数是在前两个参数构成的协议集合下,再选择一个具体的协议,一半情况下我们把它设置为0,表示使用默认协议。
NO.2 命名socket
所谓“命名”意思就是给一个套接字一个独一无二的名字,我们也将命名socket称之为绑定套接字,通常服务器在启动的时候都会绑定特定的IP和端口号,然后客户端和服务器通过绑定的这个套接字去建立通信。其函数原型如下:
#include <sys/types.h>
#include <sys/socket.h>
int bind( int sockfd, const struct scokadddr *my_addr, socklen_t addr_len ); //bind成功返回0,失败返回-1,并设置errno
该函数将my_addr所指的socket地址分配给未命名的sockfd文件描述符,addr_len参数指出该socket地址的长度。
值得讨论的是为什么my_addr要加const?
我的理解是这样的,my_addr是指向sockfd的协议地址,而这个地址是由最开始socket()函数创建得来的,之后的操作我们不允许其内容被修改,所以加了const。
NO.3 监听socket
socket被命名以后,我们不能马上接受客户端连接,所以就需要使用系统调用来创建一个监听队列以存放待处理的客户连接,具体函数如下:
#include <sys/socket.h>
int listen( int sockfd, int backlog ); //成功返回0,失败返回-1,并设置errno
参数:
backlog参数提示内核监听队列的最大长度,监听队列的长度如果超过backlog,服务器将不受理新的客户连接。在socket进行listen时,socket出现的状态只有两种,半连接状态(SYN_RECV)和完全连接状态(ESTABLISHED),而我们现在所说的backlog参数所指的其实是处于完全连接状态的socket的上限,而处于半连接状态的socket的上限被定义在/proc/sys/net/ipv4/tcp_max_syn_backlog内核参数中。
NO.4 接受连接
#include <sys/types.h>
#include <sys/socket.h>
int accept( int sockfd, struct sockaddr *addr, socklen_t *addrlen ); //成功返回一个已连接的描述字,用来唯一的标识来自某一客户端的连接;失败返回-1并设置errno
该系统调用是从listen监听队列中接受一个连接;
参数:
sockfd参数是执行过listen系统调用的监听socket; addr指向的是被接受连接的远端socket地址(客户端socket地址),addrlen参数的长度是指客户端socket地址的长度。通常服务端只创建一个监听socket,它存活的生命周期是直到整个服务端关闭;而在accept()函数执行的过程中,内核会为每一个有服务器进程接受的客户连接创建一个已连接socket,它的生命周期是完成客户端的服务,相应的已连接socket描述字也就被关闭了。
值得注意的是,accept只是从监听队列中取出连接,而不论连接出于何种状态(ESTABLISHED状态和CLOSE_WAIT状态),更不关心任何情况下的网络状况。
NO.5 发起连接
#include <sys/types.h>
#include <sys/socket.h>
int connect( int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen ); //成功返回0,失败返回-1并设置errno
客户端通过使用该系统调用主动地和服务器建立连接,参数含义如下:
sockfd参数是系统调用scoket()的返回值;serv_addr参数是服务器监听的socket地址;addrlen参数是指定这个地址的长度。
NO.6关闭连接
#include <unsitd.h>
int close( int fd );
值得注意的是这里所说的关闭连接并非是立即关闭一个连接而是将fd的引用计数减1。只有当fd的引用计数为0时,才真正关闭连接。
-----------------------------------------------华丽丽分割线------------------------------------------------------------
接下来就是数据读写了,得分为TCP和UDP两种不同类型的数据进行读写操作,当然对文件的读写操作read和write也同样适用于socket
那么就从TCP数据读写开始吧,用于TCP流数据读写的系统调用是
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv( int sockfd, void *buf, size_t len, int flags ); //接收成功返回实际读取到的数据长度,返回0意味着对方已经关闭了连接,出错返回-1并设置errno
ssize_t send( int sockfd, const void *buf, size_t len, int flags ); //发送成功返回实际写入的数据长度,失败返回-1并设置errno
参数:
recv和send函数中的buf和len分别指定读/写缓冲区的位置和大小,flags参数为数据收发提供了额外的控制,如MSG_OOB、MSG_MORE等
还有就是UDP数据读写的系统调用:
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom( int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen );
ssize_t sendto( int sockfd, const void *buf, size_t len, int flags, struct sockaddr *dest_addr, socklen_t *addrlen );
当然了除了上面TCP和UDP两个读写数据特有的系统调用以外还有一种通用的数据读写系统调用,如下:
#include <sys/socket.h>
ssize_t recvmsg( int sockfd, struct msghdr* msg, int flags );
ssize_t sendmsg( int sockfd, struct msghdr* msg, int flags );
此处应该注意的是,对数据的读取采用的是
分散读和集中写,在结构体struct msghdr中有成员struct iovec *msg_iov, 对于recvmsg而言,数据将被读取并存放在msg_iovlen块分散的内存中,这些内存的位置和长度则由msg_iov指向的数组指定,成为分散读;对于sendmsg来说,msg_iovlen块分散在内存中的数据将被一并发出,称之为集中写。
关于read函数的理解,请看我之前写的博文,浅析read()函数(man 2)返回值
write的话其实和read是差不多的,也分为阻塞和非阻塞。
先写这么多,以后再添加,,,