文章

linux网络编程-tcp通信

socket编程

网络套接字

网络套接字: socket

  • 一个文件描述符指向一个套接字(该套接字内部由内核借助两个缓冲区实现。)
  • 在通信过程中, 套接字一定是成对出现的。

前置知识

网络字节序:

image-20240913150126883

小端法:(pc本地存储) 高位存高地址。地位存低地址。 int a = 0x12345678

大端法:(网络存储) 高位存低地址。地位存高地址。

htonl --> 本地--》网络 (IP)			192.168.1.11 --> string --> atoi --> int --> htonl --> 网络字节序

htons --> 本地--》网络 (port)

ntohl --> 网络--》 本地(IP)

ntohs --> 网络--》 本地(Port)

IP地址转换函数:

int inet_pton(int af, const char *src, void *dst);		本地字节序(string IP) ---> 网络字节序

	af:AF_INET、AF_INET6

	src:传入,IP地址(点分十进制)

	dst:传出,转换后的 网络字节序的 IP地址。 

	返回值:

		成功: 1

		异常: 0, 说明src指向的不是一个有效的ip地址。

		失败:-1

   const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);	网络字节序 ---> 本地字节序(string IP)

	af:AF_INET、AF_INET6

	src: 网络字节序IP地址

	dst:本地字节序(string IP)

	size: dst 的大小。

	返回值: 成功:dst。 	

		失败:NULL

sockaddr地址结构:

IP + port --> 在网络环境中唯一标识一个进程。

struct sockaddr_in addr;

addr.sin_family = AF_INET/AF_INET6				man 7 ip

addr.sin_port = htons(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函数:

#include <sys/socket.h>

int socket(int domain, int type, int protocol);		创建一个 套接字

​	domain:AF_INET、AF_INET6、AF_UNIX

​	type:SOCK_STREAM、SOCK_DGRAM

​	protocol: 0 

​	返回值:

​		成功: 新套接字所对应文件描述符

​		失败: -1 errno

bind

#include <arpa/inet.h>

 int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);		给socket绑定一个 地址结构 (IP+port)

​	sockfd: socket 函数返回值

​		struct sockaddr_in addr;

​		addr.sin_family = AF_INET;

​		addr.sin_port = htons(8888);

​		addr.sin_addr.s_addr = htonl(INADDR_ANY);

​	addr: 传入参数(struct sockaddr *)&addr

​	addrlen: sizeof(addr) 地址结构的大小。

​	返回值:

​		成功:0

​		失败:-1 errno

listen

int listen(int sockfd, int backlog);		设置同时与服务器建立连接的上限数。(同时进行3次握手的客户端数量)

​	sockfd: socket 函数返回值

​	backlog:上限数值。最大值 128.

​	返回值:

​		成功:0

​		失败:-1 errno	

accept

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);	阻塞等待客户端建立连接,成功的话,返回一个与客户端成功连接的socket文件描述符。

​	sockfd: socket 函数返回值

​	addr:传出参数。成功与服务器建立连接的那个客户端的地址结构(IP+port)

​		socklen_t clit_addr_len = sizeof(addr);

​	addrlen:传入传出。 &clit_addr_len

​		 入:addr的大小。 出:客户端addr实际大小。

​	返回值:

​		成功:能与客户端进行数据通信的 socket 对应的文件描述。

​		失败: -1 , errno

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_adrr.sin_addr.s_addr);

​	addr:传入参数。服务器的地址结构
		
​		addrlen:服务器的地址结构的大小
​	
​		返回值:
​	

​		成功:0

​		失败:-1 errno

​	如果不使用bind绑定客户端地址结构, 采用"隐式绑定".

TCP通信流程分析:

server:

  1. socket() 创建socket

  2. bind() 绑定服务器地址结构

  3. listen() 设置监听上限

  4. accept() 阻塞监听客户端连接

  5. read(fd) 读socket获取客户端数据

  6. 小--大写 toupper()

  7. write(fd)

  8. close();

client:

  1. socket() 创建socket

  2. connect(); 与服务器建立连接

  3. write() 写数据到 socket

  4. read() 读转换后的数据。

  5. 显示读取结果

  6. close()

多进程并发服务器

多进程并发服务器:server.c

1. Socket();		创建 监听套接字 lfd
2. Bind()	绑定地址结构 Strcut scokaddr_in addr;
3. Listen();	
4. while (1) {

	cfd = Accpet();			接收客户端连接请求。
	pid = fork();
	if (pid == 0){			子进程 read(cfd) --- 小-》大 --- write(cfd)

		close(lfd)		关闭用于建立连接的套接字 lfd

		read()
		小--大
		write()

	} else if (pid > 0) {	

		close(cfd);		关闭用于与客户端通信的套接字 cfd	
		contiue;
	}
  }

5. 子进程:

	close(lfd)

	read()

	小--大

	write()	

   父进程:

	close(cfd);

	注册信号捕捉函数:	SIGCHLD

	在回调函数中, 完成子进程回收

		while (waitpid());

多线程并发服务器

多线程并发服务器: server.c

1. Socket();		创建 监听套接字 lfd

2. Bind()		绑定地址结构 Strcut scokaddr_in addr;

3. Listen();		

4. while (1) {		

	cfd = Accept(lfd, );

	pthread_create(&tid, NULL, tfn, (void *)cfd);

	pthread_detach(tid);  				// pthead_join(tid, void **);  新线程---专用于回收子线程。
  }

5. 子线程:

	void *tfn(void *arg) 
	{
		// close(lfd)			不能关闭。 主线程要使用lfd

		read(cfd)

		小--大

		write(cfd)

		pthread_exit((void *)10);	
	}

多路IO转接

image-20240913153340685

select:

原理: 借助内核, select 来监听, 客户端连接、数据通信事件。

image-20240913153143210

FD_ZERO

void FD_ZERO(fd_set *set);	--- 清空一个文件描述符集合。

​	fd_set rset;

​	FD_ZERO(&rset);

FD_SET

void FD_SET(int fd, fd_set *set);	--- 将待监听的文件描述符,添加到监听集合中

​	FD_SET(3, &rset);	FD_SET(5, &rset);	FD_SET(6, &rset);

FD_CLR

void FD_CLR(int fd, fd_set *set);	--- 将一个文件描述符从监听集合中 移除。

​	FD_CLR(4, &rset);

FD_ISSET

int  FD_ISSET(int fd, fd_set *set);	--- 判断一个文件描述符是否在监听集合中。

​	返回值: 在:1;不在:0;

​	FD_ISSET(4, &rset);

int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);

​	nfds:监听的所有文件描述符中,最大文件描述符+1

​	readfds: 读 文件描述符监听集合。	传入、传出参数

​	writefds:写 文件描述符监听集合。	传入、传出参数		NULL

​	exceptfds:异常 文件描述符监听集合	传入、传出参数		NULL

​	timeout: 	> 0: 	设置监听超时时长。

​			NULL:	阻塞监听

​			0:	非阻塞监听,轮询
​	返回值:

​		> 0:	所有监听集合(3个)中, 满足对应事件的总数。

​		0:	没有满足监听条件的文件描述符

​		-1: 	errno

思路分析:

int maxfd = 0;

lfd = socket() ;			创建套接字

maxfd = lfd;

bind();					绑定地址结构

listen();				设置监听上限

fd_set rset, allset;			创建r监听集合

FD_ZERO(&allset);				将r监听集合清空

FD_SET(lfd, &allset);			将 lfd 添加至读集合中。

while1) {

	rset = allset;			保存监听集合

	ret  = select(lfd+1, &rset, NULLNULLNULL);		监听文件描述符集合对应事件。

	if(ret > 0) {							有监听的描述符满足对应事件
	
		if (FD_ISSET(lfd, &rset)) {				// 1 在。 0不在。

			cfd = accept();				建立连接,返回用于通信的文件描述符

			maxfd = cfd;

			FD_SET(cfd, &allset);				添加到监听通信描述符集合中。
		}

		for (i = lfd+1; i <= 最大文件描述符; i++){

			FD_ISSET(i, &rset)				有read、write事件

			read()

			小 -- 大

			write();
		}	
	}
}

select优缺点:

缺点: 监听上限受文件描述符限制。 最大 1024.

检测满足条件的fd, 自己添加业务逻辑提高小。 提高了编码难度。

优点: 跨平台。win、linux、macOS、Unix、类Unix、mips

poll:

	int poll(struct pollfd *fds, nfds_t nfds, int timeout);

​	fds:监听的文件描述符【数组】

​		struct pollfd {
​			
​			int fd:	待监听的文件描述符
​			
​			short events:	待监听的文件描述符对应的监听事件

​					取值:POLLIN、POLLOUT、POLLERR

​			short revnets:	传入时, 给0。如果满足对应事件的话, 返回 非0 --> POLLIN、POLLOUT、POLLERR
​		}

​	nfds: 监听数组的,实际有效监听个数。

​	timeout:  > 0:  超时时长。单位:毫秒。

​		  -1:	阻塞等待

​		  0:  不阻塞

​	返回值:返回满足对应监听事件的文件描述符 总个数。

image-20240913154135618

优缺点:

优点:

​ 自带数组结构。 可以将 监听事件集合 和 返回事件集合 分离。

​ 拓展 监听上限。 超出 1024限制。

缺点:

​ 不能跨平台。 Linux

​ 无法直接定位满足监听事件的文件描述符, 编码难度较大。

突破 1024 文件描述符限制:

cat /proc/sys/fs/file-max  --> 当前计算机所能打开的最大文件个数。 受硬件影响。

ulimit -a 	——> 当前用户下的进程,默认打开文件描述符个数。  缺省为 1024

修改:
	打开 sudo vi /etc/security/limits.conf, 写入:

	* soft nofile 65536			--> 设置默认值, 可以直接借助命令修改。 【注销用户,使其生效】

	* hard nofile 100000			--> 命令修改上限。

epoll:

image-20240913154201100

epoll_create

int epoll_create(int size);						创建一棵监听红黑树

​	size:创建的红黑树的监听节点数量。(仅供内核参考。)

​	返回值:指向新创建的红黑树的根节点的 fd。 

​		失败: -1 errno

epoll_ctl

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);	操作监听红黑树

​	epfd:epoll_create 函数的返回值。 epfd

​	op:对该监听红黑数所做的操作。

​		EPOLL_CTL_ADD 添加fd到 监听红黑树

​		EPOLL_CTL_MOD 修改fd在 监听红黑树上的监听事件。

​		EPOLL_CTL_DEL 将一个fd 从监听红黑树上摘下(取消监听)

​	fd:
​		待监听的fd

​	event:	本质 struct epoll_event 结构体 地址

​		成员 events:

​			EPOLLIN / EPOLLOUT / EPOLLERR

​		成员 data: 联合体(共用体):

​			int fd;	  对应监听事件的 fd

​			void *ptr; 

​			uint32_t u32;

​			uint64_t u64;		

​	返回值:成功 0; 失败: -1 errno

epoll_wait

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 	 阻塞监听。

	epfd:epoll_create 函数的返回值。 epfd

	events:传出参数,【数组】, 满足监听条件的 哪些 fd 结构体。

	maxevents:数组 元素的总个数。 1024
			
		struct epoll_event evnets[1024]
	timeout:

		-1: 阻塞

		0: 不阻塞

		>0: 超时时间 (毫秒)

	返回值:

		> 0: 满足监听的 总个数。 可以用作循环上限。

		0: 没有fd满足监听事件

		-1:失败。 errno

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;					初始化  lfd的监听属性。
tep.data.fd = lfd

epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &tep);		将 lfd 添加到监听红黑树上。

while (1) {

ret = epoll_wait(epfd, ep,1024-1);			实施监听

for (i = 0; i < ret; i++) {
	
	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, buf, n);
​		}
​	}
}

}

epoll 事件模型:

image-20240913154104016

ET模式:

​ 边沿触发:

​ 缓冲区剩余未读尽的数据不会导致 epoll_wait 返回。 新的事件满足,才会触发。

​ struct epoll_event event;

​ event.events = EPOLLIN | EPOLLET;

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_NONBLOCK;

​	fcntl(cfd, F_SETFL, flg);

优缺点:

优点:

​ 高效。突破1024文件描述符。

缺点:
不能跨平台。 Linux。

epoll 反应堆模型:

epoll ET模式 + 非阻塞、轮询 + void *ptr。

原来:	socket、bind、listen -- epoll_create 创建监听 红黑树 --  返回 epfd -- epoll_ctl() 向树上添加一个监听fd -- while1)--

	-- epoll_wait 监听 -- 对应监听fd有事件产生 -- 返回 监听满足数组。 -- 判断返回数组元素 -- lfd满足 -- Accept -- cfd 满足 

	-- read() --- 小->大 -- write回去。

反应堆:不但要监听 cfd 的读事件、还要监听cfd的写事件。

	socket、bind、listen -- epoll_create 创建监听 红黑树 --  返回 epfd -- epoll_ctl() 向树上添加一个监听fd -- while1)--

	-- 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();

			cfd --> senddata();
eventadd函数:

	将一个fd, 添加到 监听红黑树。  设置监听 read事件,还是监听写事件。

tips

网络编程中: read --- recv()

​ write --- send();

read 函数的返回值:

>0: 实际读到的字节数

=0: socket中,表示对端关闭。close()

-1:	如果 errno == EINTR   被异常终端。 需要重启。

​	如果 errno == EAGIN 或 EWOULDBLOCK 以非阻塞方式读数据,但是没有数据。  需要,再次读。

​	如果 errno == ECONNRESET  说明连接被 重置。 需要 close(),移除监听队列。

​	错误。 
	errno = “其他情况” 异常。

端口复用:

int opt = 1;		// 设置端口复用。

setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, (void *)&opt, sizeof(opt));

半关闭:

通信双方中,只有一端关闭通信。  --- FIN_WAIT_2

close(cfd);

shutdown(int fd, int how);	

	how: 	SHUT_RD	关读端

			SHUT_WR	关写端

			SHUT_RDWR 关读写

shutdown在关闭多个文件描述符应用的文件时,采用全关闭方法。close,只关闭一个。

错误处理函数:

​ 封装目的:

​ 在 server.c 编程过程中突出逻辑,将出错处理与逻辑分开,可以直接跳转man手册。

存放网络通信相关常用 自定义函数						存放 网络通信相关常用 自定义函数原型(声明)。

命名方式:系统调用函数首字符大写, 方便查看man手册
	
	  如:Listen()、Accept();

函数功能:调用系统调用函数,处理出错场景。

在 server.c 和 client.c 中调用 自定义函数

联合编译 server.c 和 wrap.c 生成 server
 
	 client.c 和 wrap.c 生成 client

readn:读 N 个字节

readline:读一行

License:  CC BY 4.0