Linux网络编程(上)

一、基础

cpp 复制代码
网络套接字:socket
    一个文件描述符指向一个套接字。
    套接字是成对出现的。(插头和插座)

网络字节序:
    小端法:(PC本地存储) 高位存高地址。低位存低地址。
    大端法:(网络存储)高位存低地址。低位存高地址。

// 二进制左边是高位
int a = 0x12345678

小端法:
地址0x00: 0x78
地址0x01: 0x56
地址0x02: 0x34
地址0x03: 0x12

大端法:
地址0x00: 0x12
地址0x01: 0x34
地址0x02: 0x56
地址0x03: 0x78

二、IP地址转换函数

cpp 复制代码
htonl: 本地 --> 网络(IP)
htons: 本地 --> 网络(port)
ntohl: 网络 --> 本地(IP)
ntohs: 网络 --> 本地(Port)

本地字节序(string IP)---> 网络字节序
int inet_pton(int af, const char *src, void *dst)  
    af:AF_FNET、AF_INET6
    src:传入,IP地址(点分十进制)
    dst:传出,转换后的网络字节序的IP地址。
返回值:
    成功:1
    异常:0,说明src指向的不是一个有效的ip地址。
    失败:-1


网络字节序 --> 本地字节序(string IP)
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
    af: AF_INET、AF_INET6
    src:网络字节序IP地址
    dst:本地字节序(string IP)
    size: dst 的大小。
返回值:
    成功:dst。
    失败:NULL

三、sockaddr结构

cpp 复制代码
struct sockaddr_in addr:
    addr.sin_family = AF_INET/AF_INET6
    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和bind函数

cpp 复制代码
创建一个套接字:
int socket(int domain, int type,int protocol);
    domain: AF_INET、AF_INET6、AF_UNIX
    type: SOCK_STREAI、SOCK_DGRAM
    protocol:0
返回值:
    成功:新套接字所对应文件描述符
    失败:-1 errno


给socket绑定一个地址结构(IP+port):
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
    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
    失败:-1errno

五、listen和accept函数

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


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

六、connect函数

cpp 复制代码
用现有的socket与服务器建立连接
int connect (int sockfd, const struct sockaddr* addr, socklen_t addrlen);
    sockfd: socket函数返回值
    addr:传入参数。服务器的地址结构
    addrlen:服务器的地址结构的大小
返回值。
    成功:0
    失败:-1 errno
如果不使用bind绑定客户端地址结构,采用"隐式绑定".

七、TCP实现客户端服务器连接

cpp 复制代码
// 服务器代码
#include<stdio.h>
#include<ctype.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<pthread.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, i;
	char buf[BUFSIZ], client_IP[1024];

	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);
	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 (i = 0; i < ret; i++) {
			buf[i] = toupper(buf[i]);
		}

		write(cfd, buf, ret);
	}

	close(lfd);
	close(cfd);

	return 0;
}

// 测试如下图
cpp 复制代码
// 客户端代码
#include<stdio.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<pthread.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);

	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 error");
	}

	while(--conter) {
		write(cfd, "hello\n", 6);
		ret = read(cfd, buf, sizeof(buf));
		write(STDOUT_FILENO, buf, ret);
		sleep(1);
	}

	close(cfd);

	return 0;
}

八、图解C-S模型


九、封装错误处理代码

cpp 复制代码
将一系列自己写的函数和错误处理代码封装到另一个程序中
cpp 复制代码
// 服务器代码
#include"wrap.h"

#define SERV_PORT 9527

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

	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);    // 调用自己写的函数

	ret = bind(lfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
    if(ret != 0){
        sys_err("bind error");
    }

	Listen(lfd, 128);    // 调用自己写的函数

    // 此处省略...

	return 0;
}
cpp 复制代码
// wrap.h
#ifndef _WRAP_H_
#define _WRAP_H_

#include<stdio.h>
#include<ctype.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<pthread.h>

void sys_err(const char* str);
int Socket(int domain, int type, int protocol);
int Listen(int sockfd, int backlog);

#endif
cpp 复制代码
// wrap.c
#include"wrap.h"

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

int Socket(int domain, int type, int protocol) {
	int n = socket(domain, type, protocol);
	if (n = -1) {
		sys_err("socket error");
		return n;
	}

	return n;
}

int Listen(int sockfd, int backlog) {
	int n = listen(sockfd, backlog);
	if (n = -1) {
		sys_err("listen error");
		return n;
	}

	return n;
}

十、多进程服务器实现

cpp 复制代码
#include<stdio.h>
#include<ctype.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<string.h>
#include<strings.h>
#include<unistd.h>
#include<errno.h>
#include<signal.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<pthread.h>

// 封装的错误处理函数
#include"wrap.h"

#define SRV_PORT 9999

void catch_child(int signum) {
	while ((waitpid(0, NULL, WNOHANG)) > 0);
	return;
}

int main(int argc, char* argv[]) {
	int lfd, cfd;
	pid_t pid;
	struct sockaddr_in srv_addr, clt_addr;
	socklen_t clt_addr_len;
	char buf[BUFSIZ];
	int ret, i;

	bzero(&srv_addr, sizeof(srv_addr));		// 将地址结构清零

	srv_addr.sin_family = AF_INET;
	srv_addr.sin_port = htons(SRV_PORT);
	srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

	lfd = Socket(AF_INET, SOCK_STREAM, 0);

	Bind(lfd, (struct sockaddr*)&srv_addr, sizeof(srv_addr));

	Listen(lfd, 128);

	clt_addr_len = sizeof(clt_addr);

	while (1) {
		cfd = Accept(lfd, (struct sockaddr*)&clt_addr, &clt_addr_len);

		pid = fork();
		if (pid < 0) {
			sys_err("fork error");
		}
		else if (pid == 0) {
			close(lfd);
			break;
		}
		else {
			struct sigaction act;

			act.sa_handler = catch_child;
			sigemptyset(&act.sa_mask);
			act.sa_flags = 0;

			ret = sigaction(SIGCHLD, &act, NULL);
			if (ret != 0) {
				perr_exit("sigaction error");
			}

			close(cfd);
			continue;
		}
	}

	if (pid = 0) {
		while (1) {
			ret = Read(cfd, buf,sizeof(buf));
			if (ret = 0) {
				close(cfd);
				exit(1);
			}

			for (i = 0; i < ret; i++) {
				buf[i] = toupper(buf[i]);
			}

			write(cfd, buf, ret);
			write(STDOUT_FILENO, buf, ret);
		}
	}

	return 0;
}

// 这样实现了多个客户端访问一个服务器,同时不会产生僵尸进程

十一、多线程服务器实现

cpp 复制代码
#include<stdio.h>
#include<string.h>
#include<arpa/inet.h>
#include<pthread.h>
#include<ctype.h>
#include<unistd.h>
#include<fcntl.h>

#include"wrap.h"

#define MAXLINE 8192
#define SERV_PORT 8000

struct s_info {		// 定义一个结构体,将地址结构跟cfd捆绑
	struct sockaddr_in cliaddr;
	int connfd;
};

void* do_work(void* arg) {
	int n, i;
	struct s_info* ts = (struct s_info*)arg;
	char buf[MAXLINE];
	char str[INET_ADDRSTRLEN];				// #define INET_ADDRSTRLEN 16

	while (1) {
		n = Read(ts->connfd, buf, MAXLINE);	// 读客户端
		if (n == 0) {
			printf("the client %d closed...\n", ts->connfd);
			break;							// 跳出循环,关闭cfd
		}

		printf("received from %s at PORT %d\n",
			inet_ntop(AF_INET, &(*ts).cliaddr.sin_addr, str, sizeof(str)),
			ntohs((*ts).cliaddr.sin_port));		// 打印客户端信息(IP/PORT)

		for (i = 0; i < n; i++) {
			buf[i] = toupper(buf[i]);			// 小写 --> 大写
		}

		Write(STDOUT_FILENO, buf, n);	// 写出至屏幕
		Write(ts->connfd, buf, n);		// 回写给客户端
	}

	Close(ts->connfd);

    /*Close函数如下:
    int n = close(fd);
    if(n == -1){
        sys_err("close error");
    }
    return n;
    */    

	return (void*)0;
}

int main(void) {
	struct sockaddr_in servaddr, cliaddr;
	socklen_t cliaddr_len;
	int listenfd, connfd;
	pthread_t tid;

	struct s_info ts[256];		// 创建结构体数组.
	int i = 0;

	listenfd = Socket(AF_INET, SOCK_STREAM, 0);	// 创建一个socket,得到lfd

	bzero(&servaddr, sizeof(servaddr));			// 地址结构清零

	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);		// 指定本地任意IP
	servaddr.sin_port = htons(SERV_PORT);				// 指定端口号

	Bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr));

	Listen(listenfd, 128);		// 设置同一时刻链接服务器上限数

	printf("Accepting client connect ...\n");

	while (1) {
		cliaddr_len = sizeof(cliaddr);
		connfd = Accept(listenfd, (struct sockaddr*)&cliaddr, &cliaddr_len);	//阻塞监听客户端链接请求
		
		ts[i].cliaddr = cliaddr;
		ts[i].connfd = connfd;

		pthread_create(&tid, NULL, do_work, (void*)&ts[i]);
		pthread_detach(tid);	//子线程分离,防止僵线程产生

		i++;
	}

	return 0;
}

十二、TCP连接图

cpp 复制代码
2MSL时长:保证最后一个ACK能成功被对端接收。
(等待期间,对端没收到我发的ACK,对端会再次发送FIN请求。)

十三、端口复用

cpp 复制代码
当服务器先关闭,再关闭客户端,再启动服务器就会出错
因为服务器主动关闭要等2MSL,期间端口还被占用

解决:在bind()之前插入以下代码

//listenfd = socket(...);
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (void*)&opt, sizeof(opt));

十四、半关闭状态

cpp 复制代码
半关闭。
通信双方中,只有一端关闭通信。---FIN_WAIT_2
close(cfd);

shutdown(int fd, int how);
how:
    SHUT_RD关读端
    SHUT_WR关写端
    SHUT_RDWR关读写

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

十五、多路IO


十六、select函数

cpp 复制代码
void FD_ZERO(fd_set *set);           ---清空一个文件描述符集合。
    fd_set rset;
    FD_ZERO(&rset);


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


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


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

十七、select实现多路IO

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<arpa/inet.h>
#include<ctype.h>

// 封装的错误处理函数
#include"wrap.h"

#define SERV_PORT 6666

int main(int argc, char* argv[]) {
	int listenfd, connfd;
	char buf[BUFSIZ];
	
	struct sockaddr_in clie_addr, serv_addr;
	socklen_t clie_addr_len;

	listenfd = Socket(AF_INET, SOCK_STREAM, 0);

	int opt = 1;
	setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

	bzero(&serv_addr, sizeof(serv_addr));

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

	Bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

	Listen(listenfd, 128);

	fd_set rset, allset;		//定义读集合,备份集合allset
	int ret, maxfd = 0, n, i, j;
	maxfd = listenfd;			//最大文件描述符

	FD_ZERO(&allset);			//清空监听集合
	FD_SET(listenfd, &allset);	//将待监听fd添加到监听集合中

	while (1) {
		rset = allset;		//备份
		ret = select(maxfd + 1, &rset, NULL, NULL, NULL);	//使用select监听
		if (ret < 0) {
			perr_exit("select error");
		}

		//listenfd满足监听的读事件
		if (FD_ISSET(listenfd, &rset)) {	
			clie_addr_len = sizeof(clie_addr);

			//建立链接,不会阻塞
			connfd = Accept(listenfd, (struct sockaddr*)&clie_addr, &clie_addr_len);

			FD_SET(connfd, &allset);	//将新产生的fd,添加到监听集合中,监听数据读事件

			if (maxfd < connfd) {
				maxfd = connfd;			//修改maxfd
			}

			if (ret == 1) {
				continue;	//说明select只返回一个,并且是listenfd,后续执行无须执行
			}
		}

		//处理满足读事件的fd
		for (i = listenfd + 1; i <= maxfd; i++) {	
			//找到满足读事件的那个fd
			if (FD_ISSET(i, &rset)) {	
				n = Read(i, buf, sizeof(buf));

				//检测到客户端已经关闭链接
				if (n == 0) {			
					Close(i);
					FD_CLR(i, &allset);		//将关闭的fd,移除出监听集合
				}
				else if (n == -1) {
					perr_exit("read error");
				}

				for (j = 0; j < n; j++) {
					buf[j] = toupper(buf[j]);
				}

				write(i, buf, n);
				write(STDOUT_FILENO, buf, n);
			}
		}
	}

	Close(listenfd);

	return 0;
}

十八、select优缺点

cpp 复制代码
缺点:
    监听上限受文件描述符限制。最大1024
    轮询fd效率太慢,一个个问,有的可能用不到也被询

优点:
    跨平台。Win、Linux、MacOS、Unix

解决:通过自定义数组放入需要被轮询的fd,避免不必要的轮询
cpp 复制代码
// 用client数组解决
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<arpa/inet.h>
#include<ctype.h>

#include"wrap.h"

#define SERV_PORT 6666

int main(int argc, char* argv[]) {
	int i, j, n, maxi;

	/*自定义数组client,防止遍历1024个文件描述符 FD_SETSIZE默认为1024*/
	int nready, client[FD_SETSIZE];
	int maxfd, listenfd, connfd, sockfd;

	/* #define INET_ADDRSTRLEN 16 */
	char buf[BUFSIZ], str[INET_ADDRSTRLEN];

	struct sockaddr_in clie_addr, serv_addr;
	socklen_t clie_addr_len;

	fd_set rset, allset;	/*set读事件文件描述符集合allset用来暂存*/

	listenfd = Socket(AF_INET, SOCK_STREAM, 0);

	int opt = 1;
	setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

	bzero(&serv_addr, sizeof(serv_addr));

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

	Bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

	Listen(listenfd, 128);

	maxfd = listenfd;	/*起初listenfd即为最大文件描述符*/

	maxi = -1;			/*将来用作client[]的下标,初始值指向0个元素之前下标位置*/

	for (i = 0; i < FD_SETSIZE; i++) {
		client[i] = -1;				/*用-1初始化client[]*/
	}

	FD_ZERO(&allset);
	FD_SET(listenfd, &allset);		/*构造select监控文件描述符集*/

	while (1) {
		rset = allset;				/*每次循环时都重新设置select监控信号集*/

		nready = select(maxfd + 1, &rset, NULL, NULL, NULL);
		if (nready < 0) {
			perr_exit("select error");
		}

		/*说明有新的客户端连接请求*/
		if (FD_ISSET(listenfd, &rset)) {
			clie_addr_len = sizeof(clie_addr);

			//建立连接,不会阻塞
			connfd = Accept(listenfd, (struct sockaddr*)&clie_addr, &clie_addr_len);

			printf("received from %s at PORT %d\n",
				inet_ntop(AF_INET, &clie_addr.sin_addr, str, sizeof(str)),
				ntohs(clie_addr.sin_port));

			for (i = 0; i < FD_SETSIZE; i++) {
				/*找client[]中没有使用的位置*/
				if (client[i] < 0) {
					client[i] = connfd;		/*保存accept返回的文件描述符到client[]里*/
					break;
				}
			}

			/*达到select能监控的文件个数上限1024*/
			if (i == FD_SETSIZE) {
				fputs("too many clients\n", stderr);
				exit(1);
			}

			FD_SET(connfd, &allset);	/*向监控文件描述符集合allset添加新的文件描述符connfd*/

			if (connfd > maxfd) {
				maxfd = connfd;			/*select第一个参数需要*/
			}

			if (i > maxi) {
				maxi = i;				/*保证maxi存的总是client[]最后一个元素下标*/
			}

			if (--nready == 0) {
				continue;
			}
		}

		/*检测哪个clients有数据就绪*/
		for (i = 0; i <= maxi; i++) {
			if ((sockfd = client[i]) < 0) {
				continue;
			}

			if (FD_ISSET(sockfd, &rset)) {
				/*当client关闭连接时,服务器端也关闭对应连接*/
				if ((n = Read(sockfd, buf, sizeof(buf))) == 0) {
					Close(sockfd);
					FD_CLR(sockfd, &allset);	/*解除select对此文件描述符的监控*/
					client[i] = -1;
				}
				else if (n > 0) {
					for (j = 0; j < n; j++) {
						buf[j] = toupper(buf[j]);
					}

					Write(sockfd, buf, n);
					Write(STDOUT_FILENO, buf, n);

					if (--nready == 0) {
						break;
					}
				}
			}
		}
	}

	Close(listenfd);

	return 0;
}
相关推荐
Janspran9 小时前
监控系统3 - LVGL
linux
Nimsolax10 小时前
Linux网络Socket编程TCP
linux·网络·tcp/ip
YoungLime11 小时前
DVWA靶场之十六:未验证的重定向漏洞(Open HTTP Redirect)
网络·安全·web安全
青草地溪水旁17 小时前
linux信号(14)——SIGALRM:从“手机闹钟”看SIGALRM:进程的非阻塞定时神器
linux·信号机制
心灵宝贝17 小时前
libopenssl-1_0_0-devel-1.0.2p RPM 包安装教程(openSUSE/SLES x86_64)
linux·服务器·数据库
XUE-521131417 小时前
路由策略与路由控制实验
运维·网络·网络协议·智能路由器
加油201918 小时前
如何快速学习一个网络协议?
网络·网络协议·学习·方法论
BullSmall18 小时前
linux zgrep命令介绍
linux·运维
emma羊羊19 小时前
【文件读写】图片木马
linux·运维·服务器·网络安全·靶场