socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用打开open –> 读写write/read –> 关闭close模式来操作。Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭).
说白了Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
socket(套接字)是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。网络套接字是IP地址与端口的组合
socket 是一种IPC方法,它允许位于同一主机(计算机)或使用网络连接起来的不同主机上的应用程序之间交换数据。
套接字描述符
其实就是一个整数,我们最熟悉的句柄是0、1、2三个,0是标准输入,1是标准输出,2是标准错误输出。0、1、2是整数表示的,对应的FILE *结构的表示就是stdin、stdout、stderr
对于每个程序系统都有一张单独的表。精确地讲,系统为每个运行的进程维护一张单独的文件描述符表。当进程打开一个文件时,系统把一个指向此文件内部数据结构的指针写入文件描述符表,并把该表的索引值返回给调用者 。应用程序只需记住这个描述符,并在以后操作该文件时使用它。操作系统把该描述符作为索引访问进程描述符表,通过指针找到保存该文件所有的信息的数据结构。
套接字API里有个函数socket,它就是用来创建一个套接字。套接字设计的总体思路是,单个系统调用就可以创建任何套接字,因为套接字是相当笼统的。一旦套接字创建后,应用程序还需要调用其他函数来指定具体细节。
文件描述符:
在linux系统中打开文件就会获得文件描述符,它是个很小的正整数。每个进程在PCB(Process Control Block)中保存着一份文件描述符表,文件描述符就是这个表的索引,每个表项都有一个指向已打开文件的指针。
文件指针:
C语言中使用文件指针做为I/O的句柄。文件指针指向进程用户区中的一个被称为FILE结构的数据结构。FILE结构包括一个缓冲区和一个文件描述符。而文件描述符是文件描述符表的一个索引,因此从某种意义上说文件指针就是句柄的句柄。
socket常用函数:
参数具体含义可以在linux环境下man func
//tcp and udp
int socket(int domain, int type, int protocol)
int bind (int sockfd, const struct sockaddr * addr, socklen_t addrlen);
int listen(int sockfd, int backlog)
//only tcp
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int shutdown(int sockfd,int howto);
int close(int fd)
//tcp
ssize_t send(int sockfd, const void *buff, size_t nbytes, int flags);
ssize_t recv(int sockfd, void *buff, size_t nbytes, int flags);
//udp
int sendto (socket s , const void * msg, int len, unsigned int flags,const struct sockaddr * to , int tolen );
int recvfrom(int sockfd,void *buf,int len,unsigned int lags,struct sockaddr *from,int *fromlen);
//tcp and udp
int getsockopt(int sockfd, int level, int optname,void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
//select
int select(int maxfdp, fd_set* readfds, fd_set* writefds, fd_set* errorfds, struct timeval* timeout);
//epoll
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
socket()函数
int socket(int protofamily, int type, int protocol); //返回sockfd
protofamily:即协议域,又称为协议族(family)。常用的协议族有,AF_INET(IPV4)、AF_INET6(IPV6)、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等。
protocol:故名思意,就是指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议
bind()函数
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。
addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。
ipv4对应的是:
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */
struct in_addr sin_addr; /* internet address */
};
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
Unix域对应的是:
#define UNIX_PATH_MAX 108
struct sockaddr_un {
sa_family_t sun_family; /* AF_UNIX */
char sun_path[UNIX_PATH_MAX]; /* pathname */
};
addrlen:对应的是地址的长度。
listen()、connect()、accept()函数
int listen(int sockfd, int backlog);
listen函数的第一个参数即为要监听的socket描述字
第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
connect函数的第一个参数即为客户端的socket描述字
第二参数为服务器的socket地址
第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
sockfd:套接口描述字,该套接口在listen()后监听连接。
addr:传出参数,返回链接客户端地址信息,含IP地址和端口号
addrlen:传入传出参数(值-结果),传入sizeof(addr)大小,函数返回时返回真正接收到地址结构体的大小
shutdown()、close()函数
int shutdown(int sockfd, int howto);
sockfd是需要关闭的socket的描述符。
how允许为shutdown操作选择以下几种方式:
SHUT_RD:关闭连接的读端。也就是该套接字不再接受数据,任何当前在套接字接受缓冲区的数据将被丢弃。进程将不能对该套接字发出任何读操作。对TCP套接字该调用之后接受到的任何数据将被确认然后无声的丢弃掉。
SHUT_WR:关闭连接的写端,进程不能在对此套接字发出写操作
SHUT_RDWR:相当于调用shutdown两次:首先是以SHUT_RD,然后以SHUT_WR。使用close中止一个连接,但它只是减少描述符的参考数,并不直接关闭连接,只有当描述符的参考数为0时才关闭连接。
shutdown可直接关闭描述符,不考虑描述符的参考数,可选择中止一个方向的连接。
int close(int sockfd);
调用close()函数来释放该socket,从而停止在该socket上的任何数据操作
select()函数
int select(int maxfd,fd_set *rdset,fd_set *wrset,fd_set *exset,struct timeval *timeout);
maxfd: 需要监视的最大的文件描述符的值+1 ;
rdset: 需要检测的可读文件描述符的集合;
wrset: 需要检测的可写文件描述符的集合;
exset: 需要检测的异常文件描述符的集合,不包括错误;
struct timeval: 用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0
fd_set的对象
FD_ZERO(fd_set *fdset) //将指定的文件描述符集清空,在对文件描述符集合进行设置前,必须对其进行初始化,如果不清空,由于在系统分配内存空间后,通常并不作清空处理,所以结果是不可知的。
FD_SET(fd_set *fdset) //用于在文件描述符集合中增加一个新的文件描述符。
FD_CLR(fd_set *fdset) //用于在文件描述符集合中删除一个文件描述符。
FD_ISSET(int fd,fd_set *fdset) //用于测试指定的文件描述符是否在该集合中。
struct timeval{
__time_t tv_sec; /* Seconds. */
__suseconds_t tv_usec; /* Microseconds. */
};
epoll()函数
int epoll_create(int size)
创建一个epoll的句柄。自从linux2.6.8之后,size参数是被忽略的
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
第一个参数是epoll_create()的返回值。
第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd
第三个参数是需要监听的fd。
第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:
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队列里
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
参数events是分配好的epoll_event结构体数组,epoll将会把发生的事件赋值到events数组中(events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存)。
maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size,
参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时。
Epoll的2种工作方式-水平触发(LT)和边缘触发(ET)
getsockopt()、setsockopt() 函数
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t*optlen);
sockfd:标识一个套接口的描述字。
level:选项定义的层次;支持SOL_SOCKET、IPPROTO_TCP、IPPROTO_IP和IPPROTO_IPV6。
optname:需设置的选项。
optval:指针,指向存放选项待设置的新值的缓冲区。
optlen:optval缓冲区长度。
int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
sockfd:标识一个套接口的描述字。
level:选项定义的层次;支持SOL_SOCKET、IPPROTO_TCP、IPPROTO_IP和IPPROTO_IPV6。
optname:需设置的选项。
optval:指针,指向存放选项待设置的新值的缓冲区。
optlen:optval缓冲区长度。
其中optname:
SO_DEBUG,打开或关闭调试信息。
当option_value不等于0时,打开调试信息,否则,关闭调试信息。它实际所做的工作是在sock->sk->sk_flag中置 SOCK_DBG(第10)位,或清SOCK_DBG位。
SO_REUSEADDR,打开或关闭地址复用功能。
当option_value不等于0时,打开,否则,关闭。它实际所做的工作是置sock->sk->sk_reuse为1或0。
SO_DONTROUTE,打开或关闭路由查找功能。
当option_value不等于0时,打开,否则,关闭。它实际所做的工作是在sock->sk->sk_flag中置或清SOCK_LOCALROUTE位。
SO_BROADCAST,允许或禁止发送广播数据。
当option_value不等于0时,允许,否则,禁止。它实际所做的工作是在sock->sk->sk_flag中置或清SOCK_BROADCAST位。
SO_SNDBUF,设置发送缓冲区的大小。
发送缓冲区的大小是有上下限的,其上限为256 * (sizeof(struct sk_buff) + 256),下限为2048字节。该操作将sock->sk->sk_sndbuf设置为val * 2,之所以要乘以2,是防止大数据量的发送,突然导致缓冲区溢出。最后,该操作完成后,因为对发送缓冲的大小 作了改变,要检查sleep队列,如果有进程正在等待写,将它们唤醒。
SO_RCVBUF,设置接收缓冲区的大小。
接收缓冲区大小的上下限分别是:256 * (sizeof(struct sk_buff) + 256)和256字节。该操作将sock->sk->sk_rcvbuf设置为val * 2。
SO_KEEPALIVE,套接字保活。
如果协议是TCP,并且当前的套接字状态不是侦听(listen)或关闭(close),那么,当option_value不是零时,启用TCP保活定时 器,否则关闭保活定时器。对于所有协议,该操
作都会根据option_value置或清 sock->sk->sk_flag中的 SOCK_KEEPOPEN位。
SO_OOBINLINE,紧急数据放入普通数据流。
该操作根据option_value的值置或清sock->sk->sk_flag中的SOCK_URGINLINE位。
SO_NO_CHECK,打开或关闭校验和。
该操作根据option_value的值,设置sock->sk->sk_no_check。
SO_PRIORITY,设置在套接字发送的所有包的协议定义优先权。Linux通过这一值来排列网络队列。
这个值在0到6之间(包括0和6),由option_value指定。赋给sock->sk->sk_priority。
SO_LINGER,如果选择此选项, close或 shutdown将等到所有套接字里排队的消息成功发送或到达延迟时间后>才会返回. 否则, 调用将立即返回。
该选项的参数(option_value)是一个linger结构:
struct linger {
int l_onoff;
int l_linger;
};
如果linger.l_onoff值为0(关闭),则清 sock->sk->sk_flag中的SOCK_LINGER位;否则,置该位,并赋sk->sk_lingertime值为 linger.l_linger。
SO_PASSCRED,允许或禁止SCM_CREDENTIALS 控制消息的接收。
该选项根据option_value的值,清或置sock->sk->sk_flag中的SOCK_PASSCRED位。
SO_TIMESTAMP,打开或关闭数据报中的时间戳接收。
该选项根据option_value的值,清或置sock->sk->sk_flag中的SOCK_RCVTSTAMP位,如果打开,则还需设sock->sk->sk_flag中的SOCK_TIMESTAMP位,同时,将全局变量
netstamp_needed加1。
SO_RCVLOWAT,设置接收数据前的缓冲区内的最小字节数。
在Linux中,缓冲区内的最小字节数是固定的,为1。即将sock->sk->sk_rcvlowat固定赋值为1。
SO_RCVTIMEO,设置接收超时时间。
该选项最终将接收超时时间赋给sock->sk->sk_rcvtimeo。
SO_SNDTIMEO,设置发送超时时间。
该选项最终将发送超时时间赋给sock->sk->sk_sndtimeo。
SO_BINDTODEVICE,将套接字绑定到一个特定的设备上。
该选项最终将设备赋给sock->sk->sk_bound_dev_if。
SO_ATTACH_FILTER和SO_DETACH_FILTER。
#include
#include
#include
#include
#include
#include
#include
#include
#define MAX_READ_LINE 1024
int udpclient(int port) {
int ret = 0;
const char *server_ip_addr = "127.0.0.1";
int server_ip_port = port;
//要发送给server的数据
const char *send_message = "quit";
//用于存储接收到的数据
char buff[MAX_READ_LINE] = {0};
int socket_fd = -1;
int recv_len = -1;
do{
//创建client端的socket套接口
socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (socket_fd < 0) {
fprintf(stderr, "socket error %s errno: %d\n", strerror(errno), errno);
ret = -1;
break;
}
//初始化sockaddr_in结构体
struct sockaddr_in u_sockaddr;
memset(&u_sockaddr, 0, sizeof(struct sockaddr_in));
u_sockaddr.sin_family = AF_INET;
u_sockaddr.sin_port = htons(server_ip_port);
inet_pton(AF_INET, server_ip_addr, &u_sockaddr.sin_addr);
socklen_t server_len=sizeof(u_sockaddr);
//向server发送数据
if(sendto(socket_fd, send_message, strlen(send_message), 0, (struct sockaddr *) &u_sockaddr, sizeof(struct sockaddr_in)) < 0){
fprintf(stderr, "send message error: %s errno : %d", strerror(errno), errno);
ret = -1;
break;
}
fprintf(stdout, "client----> udp client send msg to server: %s\n", send_message);
//取server发送数据
recv_len = recvfrom(socket_fd, buff,MAX_READ_LINE, 0, (struct sockaddr *)&u_sockaddr, &server_len);
buff[recv_len] = '\0';
fprintf(stdout, "client----> udp client recv msg from server: %s\n", buff);
}while(false);
//关闭套接字
if (socket_fd >= 0){
close(socket_fd);
}
socket_fd = -1;
return ret;
}
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MAX_READ_LINE 1024
int udpserver(int port) {
int ret = 0;
int recv_len = -1;
int server_ip_port = port;
//用于存储接收到的数据
char buff[MAX_READ_LINE] = {0};
//初始化sockaddr_in结构体
struct sockaddr_in u_sockaddr;
memset(&u_sockaddr, 0, sizeof(u_sockaddr)); //bzero(&u_sockaddr,sizeof(u_sockaddr));
u_sockaddr.sin_family = AF_INET;
u_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
u_sockaddr.sin_port = htons(server_ip_port);
socklen_t socklen = sizeof(u_sockaddr);
//创建server端的socket套接字
int server_fd = -1;
do{
server_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (server_fd < 0) {
fprintf(stderr, "socket error %s errno: %d\n", strerror(errno), errno);
ret = -1;
break;
}
//绑定
ret = bind(server_fd,(struct sockaddr *) &u_sockaddr,sizeof(u_sockaddr));
if (ret < 0) {
fprintf(stderr, "bind socket error %s errno: %d\n", strerror(errno), errno);
ret = -1;
break;
}
//接收来自客户端的链接请求
for(;;) {
//读取客户端数据到buff中
recv_len = recvfrom(server_fd, buff, MAX_READ_LINE, 0, (struct sockaddr *)&u_sockaddr, &socklen);
if (recv_len < 0) {
fprintf(stderr, "recv error %s errno: %d\n", strerror(errno), errno);
continue;
}
buff[recv_len] = '\0';
//将接收到的数据标准输出
fprintf(stdout, "server----> udp server recv msg from client: %s\n", buff);
//将数据发送到客户端
sendto(server_fd, buff,MAX_READ_LINE, 0, (struct sockaddr *)&u_sockaddr, socklen);
fprintf(stdout, "server----> udp server send msg to client: %s\n", buff);
//解决tcp服务不退出的问题
if(strcmp(buff,"quit") == 0){
fprintf(stdout, "server----> udp server will quit\n");
break;
}
}
}while(false);
//关闭套接字
if (server_fd >= 0){
close(server_fd);
}
server_fd = -1;
return 0;
}
#include
#include
#include
#include
#include
#include
#include
#include
#define MAX_READ_LINE 1024
int tcpclient(int port) {
int ret = 0;
const char *server_ip_addr = "127.0.0.1";
int server_ip_port = port;
//要发送给server的数据
const char *send_message = "quit";
//用于存储接收到的数据
char buff[MAX_READ_LINE] = {0};
int socket_fd = -1;
int recv_len = -1;
do{
//创建client端的socket套接口
socket_fd = socket(AF_INET, SOCK_STREAM, 0);
if (socket_fd < 0) {
fprintf(stderr, "socket error %s errno: %d\n", strerror(errno), errno);
ret = -1;
break;
}
//初始化sockaddr_in结构体
struct sockaddr_in t_sockaddr;
memset(&t_sockaddr, 0, sizeof(struct sockaddr_in));
t_sockaddr.sin_family = AF_INET;
t_sockaddr.sin_port = htons(server_ip_port);
inet_pton(AF_INET, server_ip_addr, &t_sockaddr.sin_addr);
//连接
if((connect(socket_fd, (struct sockaddr*)&t_sockaddr, sizeof(struct sockaddr))) < 0 ) {
fprintf(stderr, "connect error %s errno: %d\n", strerror(errno), errno);
ret = -1;
break;
}
//向server发送数据
if((send(socket_fd, send_message, strlen(send_message), 0)) < 0) {
fprintf(stderr, "send message error: %s errno : %d", strerror(errno), errno);
ret = -1;
break;
}
fprintf(stdout, "client----> tcp client send msg to server: %s\n", send_message);
//取server发送数据
recv_len = recv(socket_fd,buff,MAX_READ_LINE,0);
buff[recv_len] = '\0';
fprintf(stdout, "client----> tcp client recv msg from server: %s\n", buff);
}while(false);
//关闭套接字
if (socket_fd >= 0){
close(socket_fd);
}
socket_fd = -1;
return ret;
}
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MAX_READ_LINE 1024
int tcpserver(int port) {
int recv_len = -1;
int conn_fd = -1;
int ret = 0;
int server_ip_port = port;
//用于存储接收到的数据
char buff[MAX_READ_LINE] = {0};
//初始化sockaddr_in结构体
struct sockaddr_in t_sockaddr;
memset(&t_sockaddr, 0, sizeof(t_sockaddr));
t_sockaddr.sin_family = AF_INET;
t_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
t_sockaddr.sin_port = htons(server_ip_port);
//创建server端的socket套接字
int listen_fd = -1;
do{
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd < 0) {
fprintf(stderr, "socket error %s errno: %d\n", strerror(errno), errno);
ret = -1;
break;
}
//绑定
ret = bind(listen_fd,(struct sockaddr *) &t_sockaddr,sizeof(t_sockaddr));
if (ret < 0) {
fprintf(stderr, "bind socket error %s errno: %d\n", strerror(errno), errno);
ret = -1;
break;
}
//监听
ret = listen(listen_fd, 1024);
if (ret < 0) {
fprintf(stderr, "listen error %s errno: %d\n", strerror(errno), errno);
ret = -1;
break;
}
//接收来自客户端的链接请求
for(;;) {
conn_fd = accept(listen_fd, (struct sockaddr*)NULL, NULL);
if(conn_fd < 0) {
fprintf(stderr, "accpet socket error: %s errno :%d\n", strerror(errno), errno);
continue;
}
//读取数据到buff中
recv_len = recv(conn_fd, buff, MAX_READ_LINE, 0);
if (recv_len < 0) {
fprintf(stderr, "recv error %s errno: %d\n", strerror(errno), errno);
continue;
}
buff[recv_len] = '\0';
//将接收到的数据标准输出
fprintf(stdout, "server----> tcp server recv msg from client: %s\n", buff);
//将接收到的数据发送到客户端
send(conn_fd,(void*)buff,MAX_READ_LINE,0);
fprintf(stdout, "server----> tcp server send msg to client: %s\n", buff);
close(conn_fd);
conn_fd = -1;
//解决tcp服务不退出的问题
if(strcmp(buff,"quit") == 0){
fprintf(stdout, "server----> tcp server will quit\n");
break;
}
}
}while(false);
buff[0] = '\0';
//关闭套接字
if (listen_fd >= 0){
close(listen_fd);
}
listen_fd = -1;
return 0;
}
通过上面的例子,应该对普通socket模型有了基本的理解,不过对与tcp udp编程来说,就是个皮毛。