Socket编程基础总结,全网最全

2023-05-16

IP地址:可以在网络环境中,唯一标识一台主机
端口号:可以定位网络的一台主机上,唯一标识一个进程
ip地址+端口号:可以在网络环境中,唯一标识一个进程
在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识网络通讯中的一个进程。“IP地址+端口号”就对应一个socket。欲建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接。因此可以用Socket来描述网络连接的一对一关系。
套接字通信原理如下图所示:
image.png
**在网络通信中,套接字一定是成对出现的。**一端的发送缓冲区对应对端的接收缓冲区。

大端和小端的区别

在计算机内部,数字通常被表示为二进制数。例如,整数42在计算机中的二进制表示为00101010。
在存储数字时,计算机将其划分为字节(Byte)。每个字节通常包含8个位(bit)。对于一个多字节的数字,比如一个32位整数,它会被分为4个字节。这些字节可以按照两种不同的方式进行存储:大端模式或小端模式。
在大端模式下,最高位字节被存储在内存地址最低的位置,而最低位字节被存储在内存地址最高的位置。也就是说,数字的最高有效字节(Most Significant Byte, MSB)位于存储地址的最低位。
在小端模式下,最高位字节被存储在内存地址最高的位置,而最低位字节被存储在内存地址最低的位置。也就是说,数字的最高有效字节(MSB)位于存储地址的最高位。
举个例子,考虑一个32位整数0x12345678,它的字节表示为:

  • 大端模式:12 34 56 78
  • 小端模式:78 56 34 12

在网络编程中,通常需要将数据从一个计算机发送到另一个计算机。由于不同计算机的处理器可能采用不同的字节序,因此在网络传输数据时,需要将数据序列化为一种固定的字节序。大多数网络协议都使用网络字节序,即大端模式,来进行数据传输。因此,在网络编程中,需要注意字节序的转换,以确保正确的数据传输。
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换

#include <arpa/inet.h>

uint32_t htonl(uint32_t hostlong);//本地--》网络(IP)
uint16_t htons(uint16_t hostshort);//本地--》网络(port)
uint32_t ntohl(uint32_t netlong);//网络--》本地(IP)
uint16_t ntohs(uint16_t netshort);//网络--》本地(port)

IP地址转换函数

IP地址—>string---->atoi—>int—>htonl—>网络字节序

#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);//ip地址(本地字节序)转换为网络字节序
af:AF_INET,AF_INET6为ipw4和ipw6
src:传入,IP地址(点分十进制类型)
dst:传出,转换后的网络字节序的IP地址。
返回值:	成功   1
    	异常: 0,说明src指向的不是一个有效的IP地址
    	失败:-1
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);//网络字节序转换为ip地址
af:AF_INET,AF_INET6为ipw4和ipw6
src:传入,转换后的网络字节序的IP地址。
dst:传出,IP地址(点分十进制类型)
size:dst的大小
返回值: 成功:dst
    	失败:NULL

支持IPv4和IPv6

sockaddr地址结构:

struct sockaddr_in {
	sa_family_t sin_family; 			/* Address family */  	地址结构类型
	in_port sin_port;					 		/* Port number */		端口号
	struct in_addr sin_addr;					/* Internet address */	IP地址
	/* Pad to size of `struct sockaddr'. */
	//Internet address
	struct in_addr{
    	uint32_t s_addr;//address in network byte order
	}
};
struct sockaddr_in addr;
addr.sin_family=AF_INET/AF_INET6
addr.sin_port=hton(9527);
{int dst;
 inet_pton(AF_INET,"192.157.22.45",(void*)&dst);
addr.sin_addr.s_addr=dst;}
addr.sin_addr.s_addr=htonl(INADDR_ANY);//取出系统中有效的任意IP地址,二进制类型。
bind(fd,(struct sockaddr*)&addr,size);//类型需要强转

网络套接字函数

socket模型创建流程图

image.png

socket();//产生套接字句柄
bind();//绑定IP+port
listen();//设置监听上限(同时)
accept();//阻塞监听客户端连接,有客户来时,会生成一个新的套接字,原套接字继续监听

socket函数

#include <sys/socket.h>
int socket(int domain,int type,int protocol)//创建一个套接字
domain:AF_INET,AF_INET6,AF_UNIX
type:SOCK_STREAM,SOCK_DGRAM流式协议(Tcp),报式协议(Udp)
protocol:0
返回值:成功: 新套接字所对应文件描述符
       失败: -1 error
socket(AF_INET,SOCK_STREAM,0)

bind函数

int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen)//给socket绑定一个地址结构
sockfd:socket 返回值
        struct sockaddr_in addr;
    	addr.sin_family=AF_INET;
        addr.sin_port=hton(8888);
    	addr.sin_addr.s_addr=htonl(INADDR_ANY);
addr:(struct sockaddr*)&addr//有const为传入参数
addrlen:sizeof(addr)//地址结构的大小
返回值:
    成功: 0
    失败: -1 error

listen函数

int listen(int sockfd,int backlog)//设置同时与服务器建立连接的上限数(同时进行3次握手的客户端数量)
	sockfd:socket函数返回值
    backlog:上限数值,最大值:128
    返回值:
        	成功: 0
            失败: -1 error

accept函数

int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);//阻塞等待客户端建立连接,成功的话,返回一个与客户端成功连接的socket文件描述符
	sockfd:socket函数返回值
    addr:传出参数。成功与服务器建立连接的那个客户端的地址结构(IP+port)
    addrlen:传入传出。入:addr的大小,出:客户端addr的实际大小
	返回值:
		成功:能与服务器进行数据通信的socket对应的文件描述符
    	失败: -1,error

connect函数

int connect(int sockfd,const struct sockaddr *addr,socklen_t addrlen);//使用现有的socket与服务器建立连接
	sockfd:socket函数返回值
        struct sockaddr_in srv_addr //服务器地址结构
    	srv_addr.sin_family=AF_INET;
    	srv_addr.sin_port=9527 //跟服务器bind时设定port完全一致
        inet_pton(AF_INET,"服务器的IP地址“,&srv_addr.sin_addr.s_addr);
    addr:传入参数。服务器的地址结构
	addrlen:服务器地址结构的大小
    返回值:
        成功:0
        失败:-1 errno
如果不使用bind绑定客户端地址结构,采用"隐式绑定".

客户端和服务端的实例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <pthread.h>
#include <ctype.h>
//服务端
#define SERV_PORT 9527

void sys_err(const char *str){
    perror(str);
    exit(1);
}

int main(int argc,char *argv[]){
    int lfd=0,cfd=0;
    int ret;
    char buf[BUFSIZ],client_IP[1024];//4096

    struct sockaddr_in serv_addr,clit_addr;
    socklen_t clit_addr_len;

    serv_addr.sin_family=AF_INET;
    serv_addr.sin_port=htons(SERV_PORT);
    serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);

    lfd=socket(AF_INET,SOCK_STREAM,0);
    if(lfd==-1){
        sys_err("socket error");
    }
    bind(lfd,(struct sockaddr*)&serv_addr,sizeof(serv_addr));
    listen(lfd,128);
    clit_addr_len=sizeof(clit_addr);
    cfd=accept(lfd,(struct sockaddr*)&clit_addr,&clit_addr_len);//clit_addr_lenz既要传入又要传出,传出的是客户端的大小

    if(cfd==-1){
        sys_err("accept error");
    }
    printf("client ip:%s port:%d\n",inet_ntop(AF_INET,&clit_addr.sin_addr.s_addr,client_IP,sizeof(client_IP)),ntohs(clit_addr.sin_port));


    while(1){
        ret=read(cfd,buf,sizeof(buf));
        write(STDOUT_FILENO,buf,ret);
        for(int i=0;i<ret;i++){
            buf[i]=toupper(buf[i]);
        }
        write(cfd,buf,ret);
    }
    close(lfd);
    close(cfd);
    return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <pthread.h>
#include <ctype.h>
//客户端
#define SERV_PORT 9527

void sys_err(const char *str){
    perror(str);
    exit(1);
}

int main(int argc,char *argv[]){
    int cfd;
    int conter=10;
    char buf[BUFSIZ];

    struct sockaddr_in serv_addr;
    serv_addr.sin_family=AF_INET;
    serv_addr.sin_port=htons(SERV_PORT);
    inet_pton(AF_INET,"127.0.0.1",&serv_addr.sin_addr.s_addr);//ip地址转换为网络地址
    
    cfd=socket(AF_INET,SOCK_STREAM,0);
    if(cfd==-1){
        sys_err("socket error");
    }

    int ret=connect(cfd,(struct sockaddr*)&serv_addr,sizeof(serv_addr));
    if(ret!=0){
        sys_err("connect err");
    }
    while(--conter){
        write(cfd,"hello",5);
        sleep(1);
        ret=read(cfd,buf,sizeof(buf));
        write(STDERR_FILENO,buf,ret);
    }
    close(cfd);
}

多进程并发服务器

1.socket() 创建监听套接字lfd
2.bind() 绑定地址结构Struct sockaddr_in addr
3.Listen()
4.while(1){
cfd=Accpet();    //接收客户端连接请求
pid=fork();
if(pid==0){
close(lfd) ;   关闭用于建立连接的套接字 lfd  
read();--》大
write();
}else if(pid>0){
close(cfd);    关闭用于客户端通信的套接字 cfd

contiue;
}
}
5.子进程
close(fd)   read()--》大    write()
父进程:
注册信号捕捉函数      SIGCHLD
在回调函数中,完成子进程的回收
while(waitpid())

多线程并发服务器

1.socket() 创建监听套接字lfd
2.bind() 绑定地址结构Struct sockaddr_in addr
3.Listen()
4.while(1){
cfd=Accpet();    //接收客户端连接请求
pthread_creat(&tid,NULL,tfn,NULL);
pthread_detach(tid);//pthread_join(tid,void**);新线程--专用于回收子线程
5子线程
void *tfn(void *arg){
close(lfd) ;   关闭用于建立连接的套接字 lfd  
read();--》大
write();
}

TCP状态时序图

netstat -apn|grep client 查看端口状态
netstat -apn|grep 8000 查看各个套接字的状态(LISTEN  ESTABLISHED)

image.png
image.png
TIME_WAIT状态:只有主动关闭连接,会经历这个状态,然后在经历2MSL时长后关闭,在这2MSL时长里是不能再使用这个端口的。
2MSL时长:保证最后一个ACK能成功被对端接收(等待期间,对端没收到我发的ACK,对端会再次发送FIN请求)
端口复用

int opt=1;//设置端口复用
setsockpt(lfd,SOL_SOCKET,SO_REUSEADDR,(void*)opt,sizeof(opt));
//使用这个可以设置端口复用

半关闭:通信双方,只有一方关闭通信。-----FIN_WAIT2
close(fd)
shutdown(int fd,int how);
how:SHUT_RD 关读端 SHUT_WR 关写端 SHUT_RDWR 关读写
shutdown在关闭多个文件描述符应用的文件时,采用全关闭。close只关闭一个

select多路IO转接

#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
			fd_set *exceptfds, struct timeval *timeout);

	nfds:3开始,监控的文件描述符集里最大文件描述符加1,因为此参数会告诉内核检测前多少个文件描述符的状态
	readfds:	监控有读数据到达文件描述符集合,传入传出参数
	writefds:	监控写数据到达文件描述符集合,传入传出参数
	exceptfds:	监控异常发生达文件描述符集合,如带外数据到达异常,传入传出参数
	timeout:	定时阻塞监控时间,3种情况
				1.NULL,永远等下去
				2.设置timeval,等待固定时间
				3.设置timeval里时间均为0,检查描述字后立即返回,轮询
     返回值:>0:所有监听集合(读,写,异常)中,满足对应事件的总数
            0: 没有满足监听条件的文件描述符
            -1:errno
	struct timeval {
		long tv_sec; /* seconds */
		long tv_usec; /* microseconds */
	};
	void FD_CLR(int fd, fd_set *set); 	//把文件描述符集合里fd清0
	int FD_ISSET(int fd, fd_set *set); 	//判断文件描述符集合里fd是否置1
	void FD_SET(int fd, fd_set *set); 	//把文件描述符集合里fd位置1
	void FD_ZERO(fd_set *set); 			//把文件描述符集合里所有位清0
思路分析
lfd=socket();//创建套接字
bind();//绑定地址结构
listen();//设置监听上限
fd_set rset,allset;//创建r监听集合
FD_ZERO(&allset);//将r监听集合清空
FD_SET(lfd,&allret);//将lfd添加至读集合中
while{
    rset=allset//保存监听集合
	ret=select(lfd+1,&rset,NULL,NULL,NULL);//监听文件描述集合对应事件
	if(ret>0){
		if(FD_ISSET(lfd,&rset){//1在,0不在
        	cfd=accept();//建立连接,返回用于通信的文件描述符
            FD_SET(cfd,&allset);//添加到监听通信描述符集合中
    	}
        for(i=lfd+1;i<=最大文件描述符;i++){
        	if(FD_ISSET(i,&rset){
            	read();-》大;
                write();
            }
        }
	}
}

  1. select能监听的文件描述符个数受限于FD_SETSIZE,一般为1024,单纯改变进程打开的文件描述符个数并不能改变select监听文件个数
  2. 解决1024以下客户端时使用select是很合适的,但如果链接客户端过多,select采用的是轮询模型,会大大降低服务器响应效率,不应在select上投入更多精力
    优点:跨平台。

poll多路IO转接

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
	struct pollfd {
		int fd; /* 待监听文件描述符 */
		short events; /* 待监控的文件描述对应的监听事件 */
                    取值:POLLIN,POLLOUT,POLLERR.
		short revents; /* 监控事件中满足条件返回的事件 */
	};
	nfds 			监控数组中有多少文件描述符需要被监控
	timeout 		毫秒级等待
		-1:阻塞等,#define INFTIM -1 				Linux中没有定义此宏
		0:立即返回,不阻塞进程
		>0:等待指定毫秒数,如当前系统时间精度不够毫秒,向上取值

read函数返回值:

0:实际读到的字节数
=0:socket中,表示对端关闭。close()
-1:如果errorEINTR 被异常中断,需要重启
如果error
EAGIN或EWOULDBLOCK以非阻塞方式读数据,但是没有数据,需要再次读数据
如果error==ECONNRESET 说明连接被重置,需要close(),移除监听队列
poll缺点:不能跨平台。
突破1024文件描述符限制:

	cat /proc/sys/fs/file-max// --->当前计算机所能打开的最大文件个数,受硬件影响
    ulimit -a   //---->当前用户下的进程,默认打开文件描述符个数。缺省为1024
    修改:
        打开 sudo vi /etc/security/limits.conf //写入
        soft nofile 65536  -->设置默认值,可以直接借助命令修改,注销用户使其生效
        hard nofile 100000  -->命令修改上限

epoll

//1.创建一个epoll句柄,参数size用来告诉内核监听的文件描述符的个数,跟内存大小有关。
#include <sys/epoll.h>
	int epoll_create(int size)		size:创建的红黑树的监听节点的数目
返回值:成功:指向新创建的红黑树的根节点fd
    	失败:-1 error
//2.控制某个epoll监控的文件描述符上的事件:注册、修改、删除。
	int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
		epfd:	为epoll_creat的句柄
		op:		表示动作,用3个宏来表示:
			EPOLL_CTL_ADD (注册新的fd到epfd)EPOLL_CTL_MOD (修改已经注册的fd的监听事件)EPOLL_CTL_DEL (从epfd删除一个fd);
    	fd:待监听的fd
		event:	告诉内核需要监听的事件(本质struct epoll_event结构体的地址

		struct epoll_event {
			__uint32_t events; /* Epoll events */
			epoll_data_t data; /* User data variable */
		};
		EPOLLIN :	表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
		EPOLLOUT:	表示对应的文件描述符可以写
    	EPOLLERR:	表示对应的文件描述符发生错误
		typedef union epoll_data {
			void *ptr;
			int fd;//对应监听事件的fd
			uint32_t u32;
			uint64_t u64;
		} epoll_data_t;
//等待所监控文件描述符上有事件的产生,类似于select()调用。
#include <sys/epoll.h>
	int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
		events:	传出参数 数组,传出满足监听条件的那些fd结构体。
		maxevents:	数组元素总个数,告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,
		timeout:	是超时时间
			-1:	阻塞
			0:	立即返回,非阻塞
			>0:	指定毫秒
		返回值:	
        >0:成功返回有多少文件描述符就绪,
        0:时间到时返回0,没有fd满足监听事件
        -1:出错返回-1
epoll实现多路IO转接思路
lfd=socket();//监听连接事件lfd
bind();
listen();
int epfd=epoll_create(1024);//epfd监听红黑树的树根
struct epoll_event tep,ep[1024];tep用来设置单个fd属性,ep是epoll_wait()传出的满足监听事件的数组
tep.events=EPOLLIN;
tep.data.fd=lfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&tep);//将lfd添加到监听红黑树上
while(1){
	ret=epoll_wait(epfd,ep,1024,-1);//试试监听
    for(int i=0;i<ret;i++){//for循环帮忙判断数据是否全部监听完
    	if(ep[i].data.fd==lfd){//lfd满足读事件,有新的客户端发起连接请求
            cfd=Accept();
            tep.events=EPOLLIN;//初始化 cfd的监听事件
            tep.data.fd=cfd;
            epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&tep);
        }else{//cfd们满足读事件,有客户端写数据来
            n=read(ep[i].data.fd,buf,sizeof(buf));
            if(n==0){
            	close(ep[i].data,fd);
            	epoll_ctl(epfd,EPOLL_CTL_DEL,ep[i].data.fd,NULL);//将关闭的cfd,从监听树上取下
            }else if(n>0){-->write(ep[i].data,fd);
            }
    }

}

epoll是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,因为它会复用文件描述符集合来传递结果而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。

epoll事件模型

EPOLL事件有两种模型:
Edge Triggered (ET) 边缘触发只有数据到来才触发,不管缓存区中是否还有数据。缓冲区剩余未读尽的数据不会导致epoll_wait返回。新的事件满足,才会触发。
struct epoll_event event;
event.events=EPOLLIN|EPOLLET;

Level Triggered (LT) 水平触发只要有数据都会触发。缓冲区剩余未读尽的数据会导致epoll_wait返回。
结论:
epoll的ET模式,高效模式,但是只支持非阻塞模式

struct epoll_event event;
event.events=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&event)
int flg=fcntl(cfd,F_GETFL);
flg|=O_NDNBLOCK;
fcntl(cfd,F_SETFL,flg);

epoll反应堆模型

epollET模式+非阻塞+void *ptr
反应堆模型:不但要监听cfd的读事件,还要监听cfd的写事件

socket,bind,listen --- epoll_creat创建监听红黑树--返回epfd--epoll_ctl()向树上添加一个监听fd
--while(1)--epoll_wait()监听--对应监听fd有事件产生--返回监听满足数组--判断返回数组元素--lfd满足
--Accept--cfd满足--read()----》大--cfd从监听红黑树摘下--EPOLLOUT--回调函数--epoll_ctl()
--EPOLL_CTL_ADD重新放到红黑树上监听写事件---等待epoll_wait返回---说明cfd可写---write回去--cfd从监听树上摘下
--EPOLLIN--epoll_ctl()--EPOLL_CTL_ADD重新放到红黑树上监听事件--epoll_wait监听

eventset函数
设置回调函数: lfd—>>acceptconn();
cfd—>>recvdata();
eventadd函数
将一个fd,添加到监听红黑树,设置监听read事件,还是监听写事件

线程池在网络编程的使用

typedef struct {
    void *(*function)(void *);          /* 函数指针,回调函数 */
    void *arg;                          /* 上面函数的参数 */
} threadpool_task_t;                    /* 各子线程任务结构体 */


/* 描述线程池相关信息 */


struct threadpool_t {
    pthread_mutex_t lock;               /* 用于锁住本结构体 */    
    pthread_mutex_t thread_counter;     /* 记录忙状态线程个数de琐 -- busy_thr_num */


    pthread_cond_t queue_not_full;      /* 当任务队列满时,添加任务的线程阻塞,等待此条件变量 */
    pthread_cond_t queue_not_empty;     /* 任务队列里不为空时,通知等待任务的线程 */


    pthread_t *threads;                 /* 存放线程池中每个线程的tid。数组 */
    pthread_t adjust_tid;               /* 存管理线程tid */
    threadpool_task_t *task_queue;      /* 任务队列(数组首地址) */


    int min_thr_num;                    /* 线程池最小线程数 */
    int max_thr_num;                    /* 线程池最大线程数 */
    int live_thr_num;                   /* 当前存活线程个数 */
    int busy_thr_num;                   /* 忙状态线程个数 */
    int wait_exit_thr_num;              /* 要销毁的线程个数 */


    int queue_front;                    /* task_queue队头下标 */
    int queue_rear;                     /* task_queue队尾下标 */
    int queue_size;                     /* task_queue队中实际任务数 */
    int queue_max_size;                 /* task_queue队列可容纳任务数上限 */


    int shutdown;                       /* 标志位,线程池使用状态,true或false */
};

线程池模块分析
1.main();
创建线程池
向线程池中添加任务,借助回调函数处理任务
销毁线程池
2.pthreadpool_create();
创建线程池结构体,指针
初始化线程池结构体(N个成员变量)
创建N个任务线程
创建1个管理者线程
失败时,销毁开辟的所有空间。(释放)
3.threadpool_thread()
进入子线程回调函数
接收参数void *arg–>>pool结构体
加锁–》lock–》整个结构体锁
判断条件变量–》wait
4.adjust_thread()
循环10s执行一次。
进入管理者线程回调函数
接收参数void *arg–》pool结构体
加锁–》lock–》整个结构体锁
获取管理线程池要用的变量。task_num,live_num,busy_num
根据既定算法,使用上述3变量,判断是否应该创建销毁线程池中指定步长的线程
5.threadpool_add()
总功能:
模拟产生任务 num[20]
设置回调函数 处理任务 sleep(1)代表处理完成
内部实现:
加锁
初始化任务队列结构体成员。回调函数function arg
利用环形队列机制,实现添加任务。借助队尾指针挪移%实现
环形阻塞在条件变量上的线程
解锁
6.从3.中的wait之后继续执行,处理任务
加锁
获取任务处理回调函数,及参数
利用环形队列机制,实现处理任务,借助对头挪移%实现
唤醒阻塞在条件变量上的server
解锁
加锁
改忙线程数++
解锁
执行处理任务的线程
加锁
改忙线程数–
解锁
7.创建和销毁线程
管理者线程根据task_num,live_num,busy_num
根据既定算法,使用上述3变量,判断是否应该创建销毁线程池中指定步长的线程
如果满足创建条件
pthread_creat(); 回调函数 live_num++
如果满足销毁条件
wait_exit_thr_num=10;
signal给阻塞在条件变量上的线程发送假条件满足信号
跳转至 --170wait阻塞线程会被假信号唤醒。判断:wait_exit_thr_num>0 pthread_exit();

TCP通信和UDP通信各自的优缺点

TCP:面向连接的,可靠数据包传输。对于不稳定的网络层,采取完全弥补的通信方式。丢包重传。
优点:稳定。数据流量稳定,速度稳定,顺序
缺点:传输速度慢。相率低,不追求效率
使用场景:数据的完整性要求较高,不追求效率;大数据的传输,文件传输。
UDP:无连接的,不可靠的数据报传递,对于不稳定的网络层,采取完全不弥补的通信方式,默认还原网络状态
优点:传输速度快,效率高,开销小
缺点:不稳定
使用场景:对时效性要求较高的场合。稳定性其次
游戏,视频会议,视频电话
腾讯,华为,阿里 ----应用层数据校验协议,弥补udp的不足。

UDP实现的C/S模型
recv()/send()只能
accept();------Connect();----被舍弃
server:
lfd=socket(AF_INET,STREAM,0);    SOCK_DGRAM--报文协议
bind();
listen();--可有可无
while(1){
read(cfd,buf,sizeof)--被替换---recvform()---涵盖accept传出地址结构
 ssize_t recvfrom(int sockfd,void *buf,size_t len,int flags,struct sockaddr *src_addr,socketlen_t *addrlen);
  sockfd:lfd; buf:缓冲区地址; len:缓冲区大小; flags:0; src_addr:(struct sockaddr*)&addr传出,对端地址结构; addrlen:传入传出
  返回值:成功接收数据字节数。失败:-1 errn. 0:对端关闭。
小--write();---被替换---sendto()
 ssize_t sendto(int sockfd,const void *buf,size_t len,int flags,struct sockaddr *src_addr,socketlen_t *addrlen);
  sockfd:lfd; buf:缓冲区地址; len:缓冲区大小; flags:0; src_addr:(struct sockaddr*)&addr传入,对端地址结构; addrlen:地址结构长度
  返回值:成功写出的数据字节数。失败:-1 errn. 0:对端关闭。   
}
close();
client:
connfd=socket(AF_INET,SOCK_DGRAM,0)
sendto('服务器的地址结构',地址结构大小)
recvform()
写到屏幕
close();

本地套接字
IPC:pipe,fifo,mmap,信号,本地套接字(domain)—CS模型
对比网络编程TCP C/S模型 注意以下几点:
地址结构:sockaddr_in---->sockaddr_un

struct sockaddr_in {
__kernel_sa_family_t sin_family; 			/* Address family */  	地址结构类型
__be16 sin_port;					 	/* Port number */		端口号
struct in_addr sin_addr;					/* Internet address */	IP地址
};
struct sockaddr_un {
__kernel_sa_family_t sun_family; 		/* AF_UNIX */			地址结构类型
char sun_path[UNIX_PATH_MAX]; 		/* pathname */		socket文件名(含路径)
};
struct sockaddr_in srv_addr;--->struct sockaddr_un srv_addr;
srv_addr.sin_family=AF_INET;--->srv_addr.sun_family=AF_UNIX;
srv_addr.sin_port=htons(8888); strcpy(srv_addr.sun_path,"srv_socket")
srv_addr.sin_addr.s_addr=htonl(INADDR_ANY);       len=offsetof(struct sockaddr_un sun_path+strlen("srv.socket");

以下程序将UNIX Domain socket绑定到一个地址。

	size = offsetof(struct sockaddr_un, sun_path) + strlen(un.sun_path);
	#define offsetof(type, member) ((int)&((type *)0)->MEMBER)

3.bind()函数调用成功,会创建一个socket.因此为保证bind成功,通常我们在bind之前,可以使用unlink(“srv.socket”);
4.客户端不能依赖“隐式绑定”。并且应该在通信建立过程中,创建且初始化2个地址结构
1)client_addr---->bind();
2)server_addr—>connect();

Libevent库

优点:开源,精简,跨平台(window,Linux,maxos,unix).专注于网络编程。
源码包安装:参考 README
./configure 检查安装环境生成amkefile
make 生成.o和可执行文件
sudo make install 将必要的资源cp置系统指定目录
进入sample目录,运行demo验证库安装使用情况
编译使用库的.c时,需要加-levent选项
库名libevent.so—>/user/local/
特性:基于“事件”异步通信模型。—回调(当某个条件到达时会回调)

libevent框架:

1.创建event_base
struct event_base *event_base_new(void);
struct event_base *base=event_base_new();
2.创建事件event
常规事件 event	--> event_new(); 
缓冲区事件bufferevent --> bufferevent_socket_new();
3.将事件添加到base上
int event_add(struct event *ev, const struct timeval *tv)
4.循环监听事件满足
int event_base_dispatch(struct event_base *base);
event_base_dispatch(base);
5.释放event_base
event_base_free(base)
创建事件event:
	struct event *ev;
	struct event *event_new(struct event_base *base,evutil_socket_t fd,short what,event_callback_fn cb;  void *arg);
		base: event_base_new()返回值。
		 fd: 绑定到 event 上的 文件描述符
		what:对应的事件(r、w、e)
			EV_READ		一次 读事件
			EV_WRTIE	一次 写事件
			EV_PERSIST	持续触发。 结合 event_base_dispatch 函数使用,生效。
		cb:一旦事件满足监听条件,回调的函数。
		typedef void (*event_callback_fn)(evutil_socket_t fd,  short,  void *)
		arg: 回调的函数的参数。
		返回值:成功创建的 event
将事件添加到base上
添加事件到 event_base
	int event_add(struct event *ev, const struct timeval *tv);
		ev: event_new() 的返回值。
		tv:为NULL,不会超时,意为:一直等到事件被触发,回调函数会被调用
        	为非0,等待期间,检查事件没有被触发,时间到,回调函数依旧被调用
从event_base上摘下事件				【了解】
	int event_del(struct event *ev);
		ev: event_new() 的返回值。
销毁事件
	int event_free(struct event *ev);
		ev: event_new() 的返回值。

未决和非未决:
非未决: 没有资格被处理
未决: 有资格被处理,但尚未被处理
event_new --> event —> 非未决 --> event_add --> 未决 --> dispatch() && 监听事件被触发 --> 激活态
–> 执行回调函数 --> 处理态 --> 非未决 event_add && EV_PERSIST --> 未决 --> event_del --> 非未决
image.png

带缓冲区的事件 bufferevent

#include <event2/bufferevent.h>
read/write 两个缓冲区. 也是队列实现,读走没,先入先出.
读:有数据—>读回调函数被调用–>使用bufferevent_read()—>读数据
写:使用bufferevent_write()---->向写缓冲中写数据---->该缓冲区有数据自动写出---->写完,回调函数被调用(鸡肋)

创建、销毁bufferevent:
	struct bufferevent *ev;
	struct bufferevent *bufferevent_socket_new(struct event_base *base, evutil_socket_t fd, enum bufferevent_options options);
		base: event_base
		fd:	封装到bufferevent内的 fd
		options:BEV_OPT_CLOSE_ON_FREE(释放bufferevent是关闭底层传输端口,这将关闭底层套接字,释放底层的bufferevent
	返回: 成功创建的 bufferevent事件对象。
void  bufferevent_socket_free(struct bufferevent *ev);
给bufferevent设置回调:	
	对比event:	event_new( fd, callback );  					event_add() -- 挂到 event_base 上。
			bufferevent_socket_new(fd)  bufferevent_setcb( callback )
	void bufferevent_setcb(struct bufferevent * bufev,
				bufferevent_data_cb readcb,
				bufferevent_data_cb writecb,
				bufferevent_event_cb eventcb,
				void *cbarg );
	bufev: bufferevent_socket_new() 返回值
	readcb: 设置 bufferevent 读缓冲,对应回调  read_cb{  bufferevent_read() 读数据  }
	writecb: 设置 bufferevent 写缓冲,对应回调 write_cb {  } -- 给调用者,发送写成功通知。  可以 NULL
	eventcb: 设置 事件回调。   也可传NULL
		typedef void (*bufferevent_event_cb)(struct bufferevent *bev,  short events, void *ctx);
		void event_cb(struct bufferevent *bev,  short events, void *ctx)
		{
			。。。。。
		}
		events: BEV_EVENT_CONNECTED:请求的连接过程已经完成,实现客户端时可用
	cbarg:	上述回调函数使用的 参数。
	
        read 回调函数类型:
		typedef void (*bufferevent_data_cb)(struct bufferevent *bev, void*ctx);
		void read_cb(struct bufferevent *bev, void *cbarg )
		{
			.....
			bufferevent_read();   --- read();
		}
	bufferevent_read()函数的原型:
		size_t bufferevent_read(struct bufferevent *bev, void *buf, size_t bufsize);	
	write 回调函数类型:
		int bufferevent_write(struct bufferevent *bufev, const void *data,  size_t size); 

启动、关闭 bufferevent的 缓冲区:
	void bufferevent_enable(struct bufferevent *bufev, short events);   启动	
		events: EV_READ、EV_WRITE、EV_READ|EV_WRITE
		默认、write 缓冲是 enable、read 缓冲是 disable
			bufferevent_enable(evev, EV_READ);		-- 开启读缓冲。
连接客户端:
	socket();connect();
	int bufferevent_socket_connect(struct bufferevent *bev, struct sockaddr *address, int addrlen);
		bev: bufferevent 事件对象(封装了fd)
		address、len:等同于 connect()2/3

创建监听服务器:
	------ socket();bind();listen();accept();
	struct evconnlistener * listner
//这个函数有上述四个函数的作用
	struct evconnlistener *evconnlistener_new_bind (	
		struct event_base *base,
		evconnlistener_cb cb, 
		void *ptr, 
		unsigned flags,
		int backlog,
		const struct sockaddr *sa,
		int socklen);
	base: event_base
	cb: 回调函数。 一旦被回调,说明在其内部应该与客户端完成, 数据读写操作,进行通信。
    该回调不由我们调用,是框架自动调用,因此,知晓参数含义即可,客户端连接才能回调。
	ptr: 回调函数的参数
	flags: LEV_OPT_CLOSE_ON_FREE: 释放bufferevent时关闭底层传输端口,这将关闭底层套接字,释放底层bufferevent
            LEV_OPT_REUSEABLE:端口复用。这两个可以用“|”连接
	backlog: listen() 2参。 -1 表最大值
	sa:服务器自己的地址结构体
	socklen:服务器自己的地址结构体大小。
	返回值:成功创建的监听器。
释放监听服务器:
	void evconnlistener_free(struct evconnlistener *lev);

服务器端 libevent 创建TCP连接:

  1. 创建event_base
  2. 创建bufferevent事件对象。bufferevent_socket_new();
  3. 使用bufferevent_setcb() 函数给 bufferevent的 read、write、event 设置回调函数。
  4. 当监听的 事件满足时,read_cb会被调用, 在其内部 bufferevent_read();读
  5. 使用 evconnlistener_new_bind 创建监听服务器, 设置其回调函数,当有客户端成功连接时,这个回调函数会被调用。
  6. 封装 listner_cb() 在函数内部。完成与客户端通信。
  7. 设置读缓冲、写缓冲的 使能状态 enable、disable
  8. 启动循环 event_base_dispath();
  9. 释放连接。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Socket编程基础总结,全网最全 的相关文章

随机推荐

  • MDK软件仿真使用

    开始仿真前需要先配置环境 xff0c 如果手动创建环境不能进入仿真要考虑去看一下工程配置选项的问题 xff0c 本次仿真使用AT32F403VGT7型号 xff0c 如图 xff1a 点击开始仿真 xff0c 下面对仿真工具栏的选项进行具体
  • SK6812驱动入门

    我看数据手册一般是特性 引脚定义 外围电路 时序图以及驱动代码 特性 Top SMD内部集成高质量外控单线串行级联恒流IC xff1b 控制电 与芯片集成在SMD 3528元器件中 xff0c 构成一个完整的外控像素点 色温效果均匀且一致性
  • ESD与TVS的区别

    概念 ESD全称是Electro Static discharge 意思就是 静电释放 国际上习惯将用于静电防护的器材统称为ESD 中文名称为静电阻抗器 TVS全称是Transient Voltage Suppressor 意思是 瞬间电压
  • 开关电源三种控制模式:PWM/PFM/PSM

    1 PWM PFM PSM 三种控制模式的定义 通常来说 开关电源 xff08 DC DC xff09 有三种最常见的调制方式分别为 xff1a 脉冲宽度调制 xff08 PWM xff09 脉冲频率调制 xff08 PFM xff09 脉
  • PCB布线走直角或锐角问题研究

    首先需要说明的一点 xff0c 在正常布线的过程中还是要尽量避免布线走直角和锐角 这里只是研究深入下布线走直角或锐角会有多大的危害性 xff0c 结尾给出答案 无论是教科书还是公司的技术规范都会强调布线避免出现走直角和锐角 xff0c 不过
  • 磁珠基本原理

    概念 磁珠的全称为铁氧体磁珠滤波器 xff0c 是一种抗干扰元件 xff0c 主要功能是滤除高频噪声 xff0c 消除存在于传输线结构 xff08 电路 xff09 中的噪声 工作原理 磁珠通过阻抗吸收并以发热的形式将不需要频段的能量耗散掉
  • BUCK型DC-DC变换器

    前述 DCDC从控制手段上来说分为PWM式 谐振式以及他们的结合式 每 一种方式中从输入与输出之间是否有变压器隔离又可以分为有隔离 无隔离两类 每一类有六种拓扑结构 BUCK Boost BUCK Boost Cuk Sepic和Zeat
  • dubbo服务超时导致的异常org.apache.dubbo.remoting.TimeoutException

    1 dubbo服务超时异常提示信息如下 xff1a cause org apache dubbo remoting TimeoutException Waiting server side response timeout by scan
  • 基于TCP/IP实现串口到网络的通讯转换

    工作模式 通过串口服务器 xff0c 采集到天平的称量值发送到PC端 操作步骤 1 软件测试 测试工具 xff1a USR M0 V2 2 5 8 基础设置 xff1a 模块静态IP 设置成服务器IP xff0c HTTP服务端口 设置成4
  • 结构体的对其规则以及为什么要对其

    结构体的内存对齐规则以及为什么要对齐 内存对齐规则 span class token number 1 span 第一个成员在与结构体变量偏移量为 span class token number 0 span 的地址处 span class
  • 宏定义参数

    宏定义的参数以逗号 xff08 作为分隔符 span class token macro property span class token directive keyword include span span class token s
  • [STM32]关于环形队列的实现

    在程序中使用环形队列判断接收数据格式 xff0c 避免在中断中处理造成程序响应速度慢的问题 直接贴代码 xff1a LoopRxCommu h ifndef LOOPRXCOMMU H define LOOPRXCOMMU H includ
  • C#旅程——串口发送数据

    串口发送数据时可以一个byte一个byte的发送数据 xff0c 也可以一次性丢出 xff0c 分多次丢出的话会导致一段数据被分成多段发出 xff0c 中间的延时可能会超过2ms xff0c 与FW通讯时会出现异常 span class t
  • 【记录】一次51单片机串口乱码问题排查

    记录 一次51单片机串口乱码问题排查 项目场景问题描述原因分析解决方案结语 项目场景 在51串口收发仿真实验中使用两个单片机互相通信 xff0c 程序设定A上电1s后通过串口以16进制给B发送AA 直到B收到AA后回复BB xff0c 当A
  • IO流java基础

    二十四 IO流 24 1 File 1 1 File 类概述和构造方法 File 它是文件和目录路径名的抽象表示 文件和目录是可以通过File封装成对象的 对于File而言 其封装的并不是一个真正存在的文件 仅仅是一个路径名而已 它可以是存
  • TX2上布置vins_fusion_gpu指南

    1 参考链接 如果初次安装 xff0c 新的TX2环境 xff0c 请参考文档 https github com arjunskumar vins fusion gpu tx2 nano 2 问题记录 1 xff0c 自己的环境情况 我的环
  • Ubuntu下安装cmake

    Ubuntu下安装cmake 今天因为项目的原因需要将cmake升级一下 xff0c 原来我是按照链接没有卸载旧版本 xff0c 直接升级 但是出现一些问题 xff0c 然后我全部卸载后 xff0c 重新安装 以下就是我的安装步骤 第一步
  • AUTH:basic认证和digest认证

    Http authentication BASIC In the context of an HTTP transaction basic access authentication is a method for a web browse
  • Quick Audience组织和工作空间功能解读

    简介 xff1a Quick Audience完成了权限系统全面升级 xff0c 可以解决集团企业不同品牌 不同运营组织 xff0c 不同消费者运营的诉求 xff0c 精细化保障企业数据访问安全 xff0c 提升管控的灵活度 更多关于数智化
  • Socket编程基础总结,全网最全

    IP地址 xff1a 可以在网络环境中 xff0c 唯一标识一台主机 端口号 xff1a 可以定位网络的一台主机上 xff0c 唯一标识一个进程 ip地址 43 端口号 xff1a 可以在网络环境中 xff0c 唯一标识一个进程 在TCP