一. socket
每一个 Socket 都用一个半相关描述: {协议,本地地址,本地端口}
一个完整的 Socket 则用一个相关描述 {协议,本地地址,本地端口,远程地址,远程端口}
每一个 Socket 有一个本地的唯一 Socket 号,由操作系统分配。
1. 套接字的三种类型
流式套接字 (SOCK_STREAM ) 数据报套接字(SOCK_DGRAM) 原始套接字
无链接服务器一般都是面向事物处理到,一个请求一个应答就完成客户程序与服务程序之间的相互作用
套接字工作过程如下:服务器首先启动,通过调用 socket()建立一个套接字,然后调用
bind()将该套接字和本地网络地址联系在一起,再调用 listen()使套接字做好侦听的准备,
并规定它的请求队列的长度,之后就调用 accept()来接收连接。客户在建立套接字后就可调
用 connect()和服务器建立连接。连接一旦建立,客户机和服务器之间就可以通过调用 read()
和 write()来发送和接收数据。最后,待数据传送结束后,双方调用 close()关闭套接字。
二. 套接字的一些基本知识
1. 基本结构
struct sockaddr 这个结构用来存储套接字地址
struct sockaddr {
unsigned short sa_family; /* address族, AF_xxx */ sa_family 一般来说,都是 “AFINET”。
char sa_data[14]; /* 14 bytes 的协议地址 */
};
struct sockaddr_in { (“in” 代表 “Internet”)
short int sin_family; /* Internet地址族 */
unsigned short int sin_port; /* 端口号 */
struct in_addr sin_addr; /* Internet地址 */
unsigned char sin_zero[8]; /* 添0(和struct sockaddr一样大小)*/
};
struct in_addr { 因特网地址
unsigned long s_addr;
};
下面给出套接字字节转换程序的列表:
- htons()——“Host to Network Short” 主机字节顺序转换为网络字节顺序(对无符号短型进行操作 4 bytes)
- htonl()——“Host to Network Long” 主机字节顺序转换为网络字节顺序(对无符号长型进行操作 8 bytes)
- ntohs()——“Network to Host Short “ 网络字节顺序转换为主机字节顺序(对无符号短型进行操作 4 bytes)
- ntohl()——“Network to Host Long “ 网络字节顺序转换为主机字节顺序(对无符号长型进行操作 8 bytes)
IP 地址转换
- 首先,让我假设你有一个 struct sockaddr_in ina,并且你的 IP 是 166.111.69.52 ,你想把你的 IP 存储到 ina 中。你可以使用的函数: inet_addr() ,它能够把一个用数字和点表示 IP 地址的字符串转换成一个无符号长整型.inet_addr() 返回的地址已经是网络字节顺序了,你没有必要再去调用 htonl() 函数,
ina.sin_addr.s_addr = inet_addr(“166.111.69.52” )
- 有一个 struct in_addr 并且你想把它代表的 IP 地址打印出来(按照 数字.数字.数字.数字的格式)......
这里,你可以使用函数 inet_ntoa()(“ntoa”代表“Network to ASCII”):
printf(“%s”, inet_ntoa(ina.sin_addr));
- inet_ntoa() 使用 struct in_addr 作为一个参数,不是一个长整型值。
inet_ntoa() 返回一个字符指针,它指向一个定义在函数 inet_ntoa() 中的 static 类型字符串。所以每次你调用 inet_ntoa(),都会改变最后一次调用 inet_ntoa() 函数时得到的结果。
比如:
char *a1, a2;
a1 = inet_ntoa(ina1.sin_addr); /* this is 166.111.69.52 */
a2 = inet_ntoa(ina2.sin_addr); /* this is 166.111.69.53 */
printf(“address 1: %s\n”,a1);
printf(“address 2: %s\n”,a2);
将会显示出:
address 1: 166.111.69.53
address 2: 166.111.69.53
socket() 函数
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain , int type , int protocol);
int bind (int sockfd , struct sockaddr *my_addr , int addrlen) ;
int connect (int sockfd, struct sockaddr *serv_addr, int addrlen);
connect()的三个参数意义如下:
l sockfd :套接字文件描述符,由 socket()函数返回的。
l serv_addr 是一个存储远程计算机的 IP 地址和端口信息的结构。
l addrlen 应该是 sizeof(struct sockaddr)。
int listen(int sockfd, int backlog);
int accept(int sockfd, void *addr, int *addrlen);
- accept()函数的参数意义如下:sockfd 是正在 listen() 的一个套接字描述符。
- addr 一般是一个指向 struct sockaddr_in 结构的指针;里面存储着远程连接过来的计算机的信息(比如远程计算机的 IP 地址和端口)。
- addrlen 是 一 个 本 地 的 整 型 数 值 , 在 它 的 地 址 传 给 accept() 前 它 的 值 应 该 是sizeof(struct sockaddr_in);accept()不会在 addr 中存储多余 addrlen bytes 大小的数据。如果accept()函数在 addr 中存储的数据量不足 addrlen,则 accept()函数会改变 addrlen 的值来反应这个情况
int send(int sockfd, const void *msg, int len, int flags);
- sockfd 是代表你与远程程序连接的套接字描述符。
- msg 是一个指针,指向你想发送的信息的地址。
- len 是你想发送信息的长度。
- flags 发送标记。一般都设为 0(你可以查看 send 的 man pages 来获得其他的参数值并且明白各个参数所代表的含义)。
int recv(int sockfd, void *buf, int len, unsigned int flags);
- sockfd 是你要读取数据的套接字描述符。
- buf 是一个指针,指向你能存储数据的内存缓存区域。
- len 是缓存区的最大尺寸。
- flags 是 recv() 函数的一个标志,一般都为 0 (具体的其他数值和含义请参考 recv()的 man pages)。
当 套 接 字 已 经 打 开 但 尚 未 有 连 接 的 时 候 用setsockopt()系统调用在其上设定选项(options)。setsockopt() 调用设置选项而 getsockopt()从给定的套接字取得选项。
int getsockopt(int sockfd, int level, int name, char *value, int *optlen);
int setsockopt(int sockfd, int level, int name, char *value, int *optlen);
- sockfd 必须是一个已打开的套接字。
- level 是函数所使用的协议标准(protocol level) (TCP/IP 协议使用 IPPROTO_TCP,套接字标准的选项实用 SOL_SOCKET)。
- name 选项在套接字说明书中(man page)有详细说明。
- value 指向为 getsockopt()函数所获取的值,setsockopt()函数所设置的值的地址。
- optlen 指针指向一个整数,该整数包含参数以字节计算的长度
int getpeername(int sockfd, struct sockaddr *addr, int *addrlen);
- sockfd 是你想取得远程信息的那个套接字描述符。
- addr 是一个指向 struct sockaddr (或是 struct sockaddr_in)的指针。
- addrlen 是一个指向 int 的指针,应该赋于 sizeof(struct sockaddr)的大小。
int gethostname(char *hostname, size_t size);
- hostname 是一个指向字符数组的指针,当函数返回的时候,它里面的数据就是本地的主机的名字.
- size 是 hostname 指向的数组的长度.
#include <netdb.h>
struct hostent *gethostbyname(const char *name);
正如你所看见的,它返回了一个指向 struct hostent 的指针.Struct hostent 是这样定义的:
struct hostent {
char *h_name;
char **h_aliases;
int h_addrtype;
int h_length;
char **h_addr_list;
};
#define h_addr h_addr_list[0]
下面是上面各个域代表含义的解释:
- h_name 是这个主机的正式名称。
- h_aliases 是一个以 NULL(空字符)结尾的数组,里面存储了主机的备用名称。
- h_addrtype 是返回地址的类型,一般来说是“AF_INET”。
- h_length 是地址的字节长度。
- h_addr_list 是一个以 0 结尾的数组,存储了主机的网络地址。
#include <fcntl.h>
int fcntl (int fd, int cmd, long arg);
三. select I/O多路复用
int select(int numfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
- numfds 是 readfds ,writefds,exceptfds 中 fd 集合中文件描述符中最大的数字加上1。
- readfds 中的 fd 集合将由 select 来监视是否可以读取。
- writefds 中的 fds 集合将由 select 来监视是否可以写入。
- exceptfds 中的 fds 集合将由 select 来监视是否有例外发生。
- FD_ZERO(fd_set *set)将一个文件描述符集合清零
- FD_SET(int fd, fd_set *set)将文件描述符 fd 加入集合 set 中。
- FD_CLR(int fd, fd_set *set)将文件描述符 fd 从集合 set 中删除.
- FD_ISSET(int fd, fd_set *set)测试文件描述符 fd 是否存在于文件描述符 set 中
四. epoll I/O多路复用
epoll只有epoll_create,epoll_ctl,epoll_wait 3个系统调用
1. int epoll_create(int size)
2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注册函数,它不同于select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册 要监听的事件类型
第一个参数是epoll_create()的返回值。
第二个参数表示动作,用三个宏来表示:
- EPOLL_CTL_ADD:注册新的fd到epfd中;
- EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
- EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数是需要监听的fd。
第四个参数是告诉内核需要监听什么事
- //保存触发事件的某个文件描述符相关的数据(与具体使用方式有关)
- typedef union epoll_data {
- void *ptr;
- int fd;
- __uint32_t u32;
- __uint64_t u64;
- } epoll_data_t;
- //感兴趣的事件和被触发的事件
- struct epoll_event {
- __uint32_t events; /* Epoll events */
- epoll_data_t data; /* User data variable */
- };
events可以是以下几个宏的集合
- EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
- EPOLLOUT:表示对应的文件描述符可以写;
- EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
- EPOLLERR:表示对应的文件描述符发生错误;
- EPOLLHUP:表示对应的文件描述符被挂断;
- EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
- EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)
events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存