SOCKET通用函数
关于网络字节序和主机字节序
字节在内存中排列的顺序影响它被累加器装在成的整数的值。这就是字节序问题。字节序分为大端和小端字 节序。简单来说,大端字节序:高位(23-31bit)在内存低地址处;低位(0-7bit)在内存高地址处。小端字节序 刚好相反。
如何区分机器的字节序?
void ByteOrder()
{
union TestByte
{
short var;
char sz[2];
};
TestByte value;
value.var = 0x0102;
if (value.sz[0] == 1 && value.sz[1] == 2)
printf("big endian!\n");
else if (value.sz[0] == 2 && value.sz[1] == 1)
printf("little endian!\n");
else
printf("unknow!\n");
}
可以看出同一个数值,在不同字节序上就会有不同的表现。那么对于服务器而言,这个是致命的。解决办法是发送端总是将发送的数值转换成大端字节序发出。接收端则根据自身的情况决定是否对接收到的数据进行转换。所以大端字节序也成为网络字节序。可能同一台机器上的两个进程通信都需要考虑字节问题。例如java虚拟机采用大端字节序,一般PC采用的是小端字节序。
LINUX系统提供了4个函数完成主机字节序和网络字节序的转换。
#include <netinet/in.h>
unsigned long int htonl(unsigned long int hostlong);
unsigned short int htons(unsigned short int hostshort);
unsigned long int ntohl(unsigned long int netlong);
unsigned short int ntohs(unsigned short int netlong);
long int类型的用来IP地址的转换,short int类型的用来port的转换。
关于socket地址
#include <bits/socket.h>
struct sockaddr
{
sa_family_t sa_family;//地址族类型,与协议族对应
char sz_data[14];//socket地址值,不同的协议有不同的长度
};
//socket编程涉及到的socket地址的函数的参数一般都是用这个结构体,在实际用的时候却不是这个
协议族 | 地址族 | 地址值含义和长度 |
---|---|---|
PF_UNIX | AF_UNIX | 文件的路径名,长度可达108字节 |
PF_INET | AF_INET | 16位端口号,32位IPV4地址,6字节 |
PF_INET6 | AF_INET6 | 16位端口号,32位流表示,128位IPV6地址,32位范围ID,共26字节 |
简单介绍下专用于IPV4和IPV6的两个具体的地址结构体
struct sockaddr_in
{
sa_family_t sin_family;
u_int16_t sin_port;
struct in_addr sin_addr;
};
struct in_addr
{
u_int32_t s_addr;//用网络字节序表示
};
struct sockaddr_in6
{
sa_family_t sin6_family;
u_int16_t sin6_port;
struct in6_addr sin6_addr;
u_int32_t sin6_scope_id;
};
struct in6_addr
{
unsigned char sa_addr[16];//用网络字节序表示
};
ip地址转换函数
通常,我们习惯于用可读性较好的字符串来表示IP地址,比如192.168.0.1的字符串来表示IPV4地址,用16进制字符串表示IPV6地址。但编程中,我们需要将他们转换成整数才能使用。记录日志的时候则相反,需要把整形数值转换成可读的字符串地址。下面三个函数用于IPV4地址中的点分十进制字符串和网络字节序的整数之间的转换。
#include <arpa/inet.h>
in_addr_t inet_addr(const char* strptr);//error back INADDR_NONE
int inet_aton(const char* cp, struct in_addr* inp);//error=0
char* inet_ntoa(struct in_addr in);//内有static存储结果,不可重入
下面的这两个函数,适用于IPV4和IPV6,建议用这两个
#include <arpa/inet.h>
int inet_pton(int af, const char* src, void* dst);//success = 1,fail=0
const char* inet_ntop(int af, const void* src, char* dst, socklen_t cnt);//fail=NULL,并设置errno
创建SOCKET
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
//domain告诉系统使用哪一个底层协议族,一般TCP/IP协议族,改参数一般设置为AF_INET(IPV4),或者AF_INET6(IPV6),对于unix本地域协议族,使用AF_UNIX
//type参数指定服务类型,主要是SOCK_STREAM和SOCK_UGRAM,也就是TCP和UDP协议的区别,在linux内核版本2.6.17起,type参数可以接受SOCK_NONBLOCK和SOCK_CLOEXEC,设置非阻塞和用fork创建子进程时,在子进程中自动关闭socket,不然只能使用fcntl来设置了
//protocol这个值,一般是配合domain和type来决定的,不过这个值一般是0
//函数调用成功返回一个socket文件描述符,失败返回-1(从3开始)
命名socket
通常来说,一个socket标识一个文件描述符和一个地址,将地址绑定在socket上称为socket的命名
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen);
对于返回值:
成功返回0,失败返回-1并设置errno。
两种常见的errno:EACCES和EADDRINUSE
EACCES表示被绑的的地址是受保护的地址仅有超级用户才能使用,比如普通用户绑定到了知名的服务端口20等。
EADDRINUSE表示被绑定的地址正在使用中,比如将一个socket绑定到一个位于TIME_WAIT状态的socket地址
socket监听
一般来说,socket命名后,还不能接受客户连接,需要调用listen函数来创建一个监听队列来存放待处理的客户连接
#include <sys/socket.h>
int listen(int fd, int backlog);//成功返回0,失败返回-1
backlog参数提示内核监听队列的最大长度,如果超过了,服务器将不受理新的客户连接,客户端也将受到ECONNREFUSED的错误提示.内核2.2之后,backlog表示处理完全连接状态 (ESTABLISHED)下的socket的上限,典型值是5
接受连接
#include <sys/types.h>
#include <sys/socket.h>
int accept(int fd, struct sockaddr *addr, socklen_t *addrlen);
//失败返回-1,成功返回一个新的连接socket
//accept是系统从listen监听队列中拿出一个连接,但它不管客户端的状态,可能已经掉线了
发起连接
客户端需要主动向服务器发起连接
#include <sys/types.h>
#include <sys/socket.h>
int connect(int fd, const struct sockaddr *serv_addr, socklen_t socklen);
//serv_addr表示需要连接的服务器的地址
//connect成功返回0,失败返回-1并设置errno
connect失败的errno一般有两个很常见的错误值:ECONNEREFUSED和ETIMEDOUT.
ECONNEREFUSED:目标端口不存在
ETIMEDOUT:连接超时
关闭连接
#include <unistd.h>
int close(int fd);//0表示成功,-1表示失败,并设置errno
close系统调用仅仅是将fd的引用计数减少1,特别是父子进程,只有当fd的引用计数是0的时候,才是真正的关闭。这也是必须要在父子进程中关闭两次才行。
如果是要立马将fd关闭,而不是将socket引用计数减少1,那么可以使用
#include <sys/socket.h>
int shutdown(int fd, int howto);
//howto:SHUT_RD,SHUT_WR,SHUT_RDWR
//关闭写端的时候,fd发送缓冲区的数据会在系统关闭fd之前全部发出去
//成功返回0,失败返回-1并设置errno
数据读写
TCP数据读写
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int fd, void* buf, size_t len, int flags);
ssize_t send(int fd, const void* buf, size_t len, int flags);
UDP读写
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int fd, void* buf, size_t len, int flags, struct sockaddr* src_addr, socklen_t *addrlen);
ssize_t sendto(int fd, const void* buf, size_t len, int flags, const struct sockaddr *addr, socklen_t addrlen);
//如果最后两个参数设置NULL,可以用于STREAM的socket
通用数据读写
#include <sys/socket.h>
ssize_t recvmsg(int fd, struct msghdr *msg, int flags);
ssize_t sendmsg(int fd, struct msghdr *msg, int flags);
struct msghdr
{
void* msg_name;//socket address
socklen_t msg_namelen;//地址长度
struct iovec* msg_iov;//分散的内存块
int msg_iovlen;//分散内存块的数量
void* msg_control;//只想辅助数据的起始位置
socklen_t msg_controllen;//辅助数据的大小
int msg_flags;//复制函数中的flags参数,并在调用过程中更新
};
struct iovec
{
void* iov_base;//内存起始地址
size_t iov_len;//内存块的长度
};
外带标记
发送外带标记的数据是,对send的参数做设置即可。linux内核如果检测到了TCP紧急标记时,会通知应用程序有外带数据需要接受。发送不紧急。接受紧急。
内核通知外带数据到达的两种方式是IO复用产生的异常事件和SIGURG信号。
但是,即使应用程序得到了有外带数据需要接受的通知,还需要知道外带数据在数据流中的具体位置,才能准确接受。所以使用下面函数可以判断下一个可被读取的数据是否是外带数据。
#include <sys/socket.h>
int sockatmark(int fd);
地址信息函数
某些时候,我们需要知道一个连接socket的本端的socket地址和远端的socket地址
#include <sys/socket.h>
int getsockname(int fd, struct sockaddr *addr, socklen_t *len);
int getpeername(int fd, struct sockaddr *addr, socklen_t *len);
//成功返回0,失败返回-1并设置errno
socket选项
fcntl系统调用时用来控制文件描述符属性的通用POSIX方法,那么接下来的两个函数就是系统专门处理socket文件描述符属性的专用方法
#include <sys/socket.h>
int getsockopt(int fd,int level,int opt,void* opt_var,socklen_t *len);
int setsockopt(int fd,int level,int opt,const void* opt_var, socklen_t len);
网络信息API
关于网络信息api,之后详细讨论。
select系统调用
#include <sys/select.h>
int select(int nfds. fd_set *rfds, fd_set *wfds, fd_set *exceptfds, struct timeval *timeout);
#include <typesizes.h>
#define __FD_SETSIZE 1024
#include <sys/select.h>
#define FD_SETSIZE __FD_SETSIZE
typedef long int __fd_mask;
#undef __NFDBITS
#define __NFDBITS (8 * (int)sizeof(__fd_mask));
typedef struct
{
#ifdef __USE_XOPEN
__fd_mask fds_bits[__FD_SIZE/__NIFBITS];
#define __FDS_BITS(set) ((set)->fds)_bits
#else
__fd_mask __fds_bits[__FD_SETSIZE/__NFDBITS];
#define __FDS_BITS(set) ((set)->__fds_bits)
#endif
}fd_set;
FD_ZERO(fd_set *fdset);
FD_SET(int fd, fd_set *fdset);
FD_CLR(int fd, fd_set *fdset);
FD_ISSET(int fd, fd_set *fdset);
struct timeval
{
long tv_sec;//秒
long tv_usec;//毫秒
};//这个参数全部设置0,表示立刻返回;设置NULL,表示一直阻塞,知道有就绪通知
对于select,函数成功返回就绪的文件描述符总数。
如果在超时时间内没有就绪文件描述符,select返回0
失败返回-1,并设置errno
poll系统调用
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd
{
int fd;
short events;//注册的事件
short revents;//实际发生的事件,由系统填充
};
nfds-表示监听的fds集合的大小
timeout表示超时时间,单位是毫秒,如果设置0,表示立刻返回,-1表示永久阻塞,直到有事件发生
poll事件类型
-事件 | -描述 | -是否可作为输入 | -是否可作为输出 |
---|---|---|---|
POLLIN | 数据可读 | 是 | 是 |
POLLRDNORM | 普通数据可读 | 是 | 是 |
POLLRDBAND | 优先级带数据可读linux不支持 | 是 | 是 |
POLLPRI | 高优先级数据可读,比如tcp外带数据 | 是 | 是 |
POLLOUT | 数据可写 | 是 | 是 |
POLLWRNORM | 普通数据可写 | 是 | 是 |
POLLWRBAND | 优先级带数据可写 | 是 | 是 |
POLLRDHUP | TCP连接被对方关闭,或者对方关闭了写操作,由GNU引入 | 是 | 是 |
POLLERR | 错误 | 否 | 是 |
POLLHUP | 挂起,比如管道的写端关闭后,读端描述符上收到POLLHUP事件 | 否 | 是 |
POLLNVAL | 文件描述符没有打开 | 否 | 是 |
想使用POLLRDHUP,必须在代码最开始处定义_GNU_SOURCE
EPOLL系统调用
epoll是linux特有的IO函数。它与poll和select由很大的差别。EPOLL使用一组函数来完成任务。epoll把用户关心的文件描述符上的事件放在内核的一个事件表中,无须像select和poll那样,每次调用都需要重复传入文件描述符或者事件集。但epoll需要一个额外的文件描述符,用于表示这个内核中的事件表。
这个事件表的文件描述符用下面这个函数来创建
#include <sys/epoll.h>
int epoll_create(int size);
size这个参数不起作用,只是给内核一个提示,告诉它事件表需要多大。
操作epoll内核事件表的函数:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* ev);
op指定往epfd的事件表中做针对fd上事件的操作
EPOLL_CTL_ADD,往事件中注册fd上的事件
EPOLL_CTL_MOD,修改fd上的注册事件
EPOLL_CTL_DEL,删除fd上的注册事件
struct epoll_event
{
__uint32_t events;//epoll事件
epoll_data_t data;//用户数据
};
typedef union epoll_data
{
void* ptr;
int fd;
uint32_t u32;
uint64_t u64;
}epoll_data_t;
在一段超时时间内等待一组文件描述符上的事件
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
成功返回就绪文件描述符的个数,失败返回-1并设置errno
timeout表示超时时间,单位是毫秒,如果设置0,表示立刻返回,-1表示永久阻塞,直到有事件发生
maxevents参数指定最多监听多少事件,必须大于0
epoll_wait函数如果检测到事件,就把所有就绪的事件从内核事件表中复制到它的第二个参数指向的数组里面。这个数组只用于输出epoll_wait检测到的就绪事件,而不像select和poll的数组参数那样既用于传入用户注册的事件,又用于输出内核检测到的就绪事件,这样就提高了效率
总结:就目前socket编程通用函数一般就这些了,前期除了学习socket编程的时候了解各个函数的基本用途能用到之外。到后面一般都封装在网络库里面了,用到这些函数的机会是不多的。总之,打好基础!