主机字节序和网络字节序
不同的CPU有不同的字节序类型 这些字节序是指整数在内存中保存的顺序 这个叫做主机字节序。最常见的有两种:
1. Little endian:将低序字节存储在起始地址
2. Big endian:将高序字节存储在起始地址
网络字节顺序是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。网络字节顺序采用big endian排序方式。
为了进行转换 bsd socket提供了转换的函数 有下面四个:
htons 把unsigned short类型从主机序转换到网络序
htonl 把unsigned long类型从主机序转换到网络序
ntohs 把unsigned short类型从网络序转换到主机序
ntohl 把unsigned long类型从网络序转换到主机序
Socket编程主要函数
int socket(int family, int type, int protocol);
描述:
建立一个socket描述符。
参数:
family指明所使用的协议族,通常为PF_INET
type参数指定socket的类型
protocol:通常为0
返回值:
Socket描述符
int connect(int sockfd, const struct sockaddr *serveraddr, socklen_t addrlen);
描述:
TCP客户端向服务端发起连接请求。客户端不需要调用bind函数,因为协议栈会自动分配一个临时的端口。
参数:
sockfd是socket函数返回的描述符
sockaddr是服务端的socket地址
addrlen是sockaddr的size
返回值:
错误码
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
描述:
为本端socket绑定IP地址和端口。(Server端肯定要调用,Client端不必需)
参数:
sockfd是socket函数返回的描述符
sockaddr是本端的socket地址,指定IP地址和端口
addrlen是sockaddr的size
返回值:
错误码
int listen(int sockfd, int backlog);
描述:
TCP服务端建立侦听。
参数:
sockfd是socket函数返回的描述符
backlog:曾被定义为(未完成3次握手的客户端连接数+以完成3次握手的客户端连接数),未有统一定义。
返回值:
错误码
int accept(int sockfd, struct sockadr *cliaddr, socklen_t addrlen);
描述:
TCP服务端接受一个客户端连接。实际实现中,在一个死循环中调用该函数,如果没有接收到连接,函数阻塞,如果接收到连接,函数返回客户端socket地址,服务端进程调用fork函数派生子进程处理新连接。父进程关闭client连接套接字,子进程则关闭侦听。
参数:
sockfd是socket函数返回的描述符
cliaddr是客户端socket地址
addrlen是cliaddr的size
返回值:
client端socket描述符
int close(int sockfd);
描述:
关闭一个socket
参数:
sockfd是要关闭的socket
返回值:
错误码
int send(int sockfd, const void *msg, int len, int flags);
描述:
向对端发送数据
参数:
sockfd:对端socket
msg:发送的数据缓存指针
len:发送的数据size
flags:一般为0
返回值:
实际发送的数据size,-1表示出错
int recv(int sockfd,void *buf,int len,unsigned int flags);
描述:
从对端接收数据
参数:
sockfd:本端socket描述符
buf:接收的数据缓冲区
len:允许接收的最大数据size
flags:一般为0
返回值:
实际接收的数据size,-1表示出错
异步TCP Socket编程原理
你可以使用select, poll, epoll,并且将socket fd设置为non-blocking. 或者用异步通知,用信号处理程序来处理客户端发来的数据
允许你把进程本身挂起来,而同时使系统内核监听所要求的一组文件描述符的任何活动,只要确认在任何被监控的文件描述符上出现活动,select()调用将返回指示该文件描述符已准备好的信息,从而实现了为进程选出随机的变化,而不必由进程本身对输入进行测试而浪费 CPU开销。
select
select最早于1983年出现在4.2BSD中,它通过一个select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述符从而进行后续的读写操作。
select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点,事实上从现在看来,这也是它所剩不多的优点之一。
select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制。
另外,select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销。
poll
poll在1986年诞生于System V Release 3,它和select在本质上没有多大差别,但是poll没有最大文件描述符数量的限制。
poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。
另外,select()和poll()将就绪的文件描述符告诉进程后,如果进程没有对其进行IO操作,那么下次调用select()和poll()的时候将再次报告这些文件描述符,所以它们一般不会丢失就绪的消息,这种方式称为水平触发(Level Triggered)。
epoll
直到Linux2.6才出现了由内核直接支持的实现方法,那就是epoll,它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。
epoll可以同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能要更高一些,但是代码实现相当复杂。
epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。
另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。
int select(int numfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);
其中readfds、writefds、exceptfds分别是被select()监视的读、写和异常处理的文件描述符集合。如果你希望确定是否可以从标准输入和某个socket描述符读取数据,你只需要将标准输入的文件描述符0和相应的sockdtfd加入到readfds集合中;numfds的值是需要检查的号码最高的文件描述符加1,这个例子中numfds的值应为sockfd+1;当select返回时,readfds将被修改,指示某个文件描述符已经准备被读取,你可以通过FD_ISSSET()来测试。为了实现fd_set中对应的文件描述符的设置、复位和测试,它提供了一组宏:
FD_ZERO(fd_set *set)----清除一个文件描述符集;
FD_SET(int fd,fd_set *set)----将一个文件描述符加入文件描述符集中;
FD_CLR(int fd,fd_set *set)----将一个文件描述符从文件描述符集中清除;
FD_ISSET(int fd,fd_set *set)----试判断是否文件描述符被置位。
Timeout参数是一个指向struct timeval类型的指针,它可以使select()在等待timeout长时间后没有文件描述符准备好即返回。struct timeval数据结构为:
struct timeval {
int tv_sec;
int tv_usec;
};
TCP socket编程基本过程
TCP Server:socket() - bind() -listen() - accept() - send() - recv() - close()
TCP Client:socket() - connect - send() recv() - close()
UDP socket编程基本过程
UDP Server:socket() - bind() - sendto() - recvfrom()
UDP Client:socket() - sendto() - close()