实现TCP通信(含地址快速重用介绍)

地址快速重用

地址快速重用(Address reuse)是指在计算机网络通信中,使用相同的源IP地址和源端口号发送多个不同的数据包。这种技术通常用于加快网络连接的建立和数据传输速度。

在TCP/IP协议中,每个TCP连接都需要独立的四元组(源IP地址、源端口号、目的IP地址、目的端口号)来进行标识和识别。如果使用地址快速重用技术,可以避免频繁地分配和释放端口号,从而加快网络连接建立和数据传输的速度。

需要注意的是,使用地址快速重用技术可能会导致一些安全风险,因为攻击者可以利用相同的源IP地址和源端口号来伪造数据包,进行网络攻击。因此,在实际应用中需要进行一些安全措施来避免这种情况的发生。

在网络编程中,若想使用已被占用的地址和端口,将导致绑定套接字到该地址的操作失败,并返回错误。为了能够成功绑定到被占用的地址和端口,需要进行相应的设置,即启用地址重用功能。

系统默认阻止重复绑定有其道理,而地址重用也有其存在的意义。我们都知道,在网络中传输的消息都有一个最长的存活时间,如果在这个时间段内没有达到没目的地就会被丢弃。在服务器正常关闭与意外崩溃后,在网络中有可能依旧存在以该地址和端口为目的地的信息,操作系统通过保留一段时间的套接字资源让这些滞留在网络中的信息自行消散。这样做是可以防止历史连接过过程中的数据对下一次连接造成干扰和影响,但是却不利于我们高效利用网络资源,尤其是在一些频繁绑定与释放网络任务中,这些暂留的时间会导致产生大量无法被有效利用的网络资源。在具体的网络情景中,我们需要根据需要对是否使用网络重用进行取舍。

        实现地址重用可以使用setsockopt()函数,并指定SO_REUSEADDR 选项。以下是一个开启地址重用功能的例子:

#include <sys/socket.h>
#include <netinet/in.h>
 
int reuse_addr = 1;
setsockopt(socket_fd,SOL_SOCKET,SO_REUSEADDR,&reuse_addr,sizeof(reuse_addr));

下面是对地址重用所使用函数的参数解释:

  • socket_fd:指定要设置选项的套接字文件描述符。
  • SOL_SOCKET:指定选项所属的协议级别,这里是套接字级别。
  • SO_REUSEADDR:指定要设置的选项类型,即地址重用选项。
  • &reuse_addr:传入一个指向存储选项值的变量的指针,这里是启用地址重用
  • sizeof(reuse_addr):指定选项值的大小。

socket函数 与 通信域

#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
//-domain: 指定通信域(通信地址族);
//-type: 指定套接字类型;
//-protocol: 指定协议;

-domain通信地址族:AF_INET: 使用IPv4 互联网协议  AF_INET6: 使用IPv6 互联网协议 ......

-type: 指定套接字类型

TCP唯一对应流式套接字,所以选择SOCK_STREAM(数据报套接字:SOCK_DGRAM)

-protocol: 指定协议 流式套接字唯一对应TCP,所以无需要指定协议,设为0即可

bind函数 与 通信结构体

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
//-sockfd:socket函数生成的套接字
//-addr:通信结构体
//-addrlen:通信结构体的长度

通信地址族 与 通信结构体的关系

//通用地址族结构体
struct sockaddr {
        sa_family_t sa_family;
        char        sa_data[14];
}

//IPv4地址族结构体
struct sockaddr_in {
        sa_family_t    sin_family; /* 地址族: AF_INET */
        in_port_t      sin_port;   /* 网络字节序的端口号 */
        struct in_addr sin_addr;   /*IP地址结构体 */
};
/* IP地址结构体 */
struct in_addr {
        uint32_t       s_addr;     /* 网络字节序的IP地址 */
};

示例:为套接字fd绑定通信结构体addr

addr.sin_family = AF_INET;
addr.sin_port = htons(5001); //绑定端口号 主机转网络
addr.sin_addr.s_addr = 0;
bind(fd, (struct sockaddr *)&addr, sizeof(addr) ); //通信结构体绑定到套接字上

listen函数 与 accept函数

/*监听套接字*/
int listen(int sockfd, int backlog);

/*处理客户端发起的连接,生成新的套接字*/
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
//-sockfd: 函数socket生成的套接字
//-addr:客户端的地址族信息
//-addrlen:地址族结构体的长度

服务器端示例代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>


#define PORT 5001
#define BACKLOG 5

int main(){
        int fd,newfd;
        char buf[BUFSIZ] = {};
        struct sockaddr_in addr;

        fd = socket(AF_INET,SOCK_STREAM,0);
        if(fd<0){
                perror("socket");
                exit(0);
        }

        addr.sin_family = AF_INET;
        addr.sin_port = htons(PORT); //绑定端口号 主机转网络
        addr.sin_addr.s_addr = 0;
        if(bind(fd, (struct sockaddr *)&addr, sizeof(addr) )==-1){ //通信结构体>绑定到套接字上  
                perror("bind");
                exit(0);
        }
        if(listen(fd,BACKLOG) == -1){
                perror("listen");
                exit(0);
        }
        newfd=accept(fd,NULL,NULL);
        if(newfd<0){
                perror("socket");
                exit(0);
        }
        printf("BUFSIZ = %d\n",BUFSIZ);
        read(newfd,buf,BUFSIZ);
        printf("buf = %s\n",buf);
        close(fd);
        return 0;
}

客户端示例代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>


#define PORT 5001
#define BACKLOG 5
#define STR "HELLO WORLD!"

int main(){
        int fd;

        struct sockaddr_in addr;

        fd = socket(AF_INET,SOCK_STREAM,0);
        if(fd<0){
                perror("socket");
                exit(0);
        }

        addr.sin_family = AF_INET;
        addr.sin_port = htons(PORT); //绑定端口号 主机转网络
        addr.sin_addr.s_addr = inet_addr("127.0.0.1");
        if(connect(fd, (struct sockaddr *)&addr, sizeof(addr) )==-1){ //通信结构
体>绑定到套接字上  
                perror("connect");
                exit(0);
        }

        write(fd,STR,sizeof(STR));
        printf("STR = %s\n",STR);
        close(fd);
        return 0;
}

运行截图:

代码优化

客户端:

#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>

#define BACKLOG 5

int main(int argc, char *argv[])
{
	int fd;
	struct sockaddr_in addr;
	char buf[BUFSIZ] = {};

	if(argc < 3){
		fprintf(stderr, "%s<addr><port>\n", argv[0]);
		exit(0);
	}

	/*创建套接字*/
	fd = socket(AF_INET, SOCK_STREAM, 0);
	if(fd < 0){
		perror("socket");
		exit(0);
	}

	addr.sin_family = AF_INET;
	addr.sin_port = htons( atoi(argv[2]) );
	if ( inet_aton(argv[1], &addr.sin_addr) == 0) {
		fprintf(stderr, "Invalid address\n");
		exit(EXIT_FAILURE);
	}

	/*向服务端发起连接请求*/
	if(connect(fd, (struct sockaddr *)&addr, sizeof(addr) ) == -1){
		perror("connect");
		exit(0);
	}
	while(1){
		printf("Input->");
		fgets(buf, BUFSIZ, stdin);
		write(fd, buf, strlen(buf) );
	}
	close(fd);
	return 0;
}

服务器:

#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>

#define BACKLOG 5

int main(int argc, char *argv[])
{
	int fd, newfd, ret;
	char buf[BUFSIZ] = {}; //BUFSIZ 8142
	struct sockaddr_in addr;
	
	if(argc < 3){
		fprintf(stderr, "%s<addr><port>\n", argv[0]);
		exit(0);
	}

	/*创建套接字*/
	fd = socket(AF_INET, SOCK_STREAM, 0);
	if(fd < 0){
		perror("socket");
		exit(0);
	}
	addr.sin_family = AF_INET;
	addr.sin_port = htons( atoi(argv[2]) );
	if ( inet_aton(argv[1], &addr.sin_addr) == 0) {
		fprintf(stderr, "Invalid address\n");
		exit(EXIT_FAILURE);
	}

	/*绑定通信结构体*/
	if(bind(fd, (struct sockaddr *)&addr, sizeof(addr) ) == -1){
		perror("bind");
		exit(0);
	}
	/*设置套接字为监听模式*/
	if(listen(fd, BACKLOG) == -1){
		perror("listen");
		exit(0);
	}
	/*接受客户端的连接请求,生成新的用于和客户端通信的套接字*/
	newfd = accept(fd, NULL, NULL);
	if(newfd < 0){
		perror("accept");
		exit(0);
	}
	while(1){
		memset(buf, 0, BUFSIZ);
		ret = read(newfd, buf, BUFSIZ);
		if(ret < 0)
		{
			perror("read");
			exit(0);
		}
		else if(ret == 0)
			break;
		else
			printf("buf = %s\n", buf);
	}
	close(newfd);
	close(fd);
	return 0;
}

TCP多进程并发:(服务器接收多客户端消息)

#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>

#define BACKLOG 5  // 最大同时连接请求数

void ClinetHandle(int newfd);  // 声明用于处理客户端的函数

int main(int argc, char *argv[])
{
	int fd, newfd;  // fd 是监听套接字,newfd 是与客户端通信的套接字
	struct sockaddr_in addr, clint_addr;  // 服务器和客户端的地址结构体
	socklen_t addrlen = sizeof(clint_addr);
	
	pid_t pid;  // 用于存放进程ID
	
	if(argc < 3){
		fprintf(stderr, "%s<addr><port>\n", argv[0]);  // 命令行参数错误提示
		exit(0);
	}

	// 创建套接字
	fd = socket(AF_INET, SOCK_STREAM, 0);
	if(fd < 0){
		perror("socket");
		exit(0);
	}
	addr.sin_family = AF_INET;
	addr.sin_port = htons(atoi(argv[2]));  // 设置端口
	if (inet_aton(argv[1], &addr.sin_addr) == 0) {  // 设置IP地址
		fprintf(stderr, "Invalid address\n");
		exit(EXIT_FAILURE);
	}

	// 设置地址重用
	int flag=1, len= sizeof(int); 
	if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &flag, len) == -1) { 
		perror("setsockopt"); 
		exit(1); 
	} 
	
	// 绑定套接字
	if(bind(fd, (struct sockaddr *)&addr, sizeof(addr)) == -1){
		perror("bind");
		exit(0);
	}

	// 将套接字设置为监听模式
	if(listen(fd, BACKLOG) == -1){
		perror("listen");
		exit(0);
	}
	while(1){
		// 接受客户端的连接请求
		newfd = accept(fd, (struct sockaddr *)&clint_addr, &addrlen);
		if(newfd < 0){
			perror("accept");
			exit(0);
		}
		// 打印客户端地址和端口信息
		printf("addr:%s port:%d\n", inet_ntoa(clint_addr.sin_addr), ntohs(clint_addr.sin_port));
		
		// 创建子进程来处理客户端
		if((pid = fork()) < 0){
			perror("fork");
			exit(0);
		}else if(pid == 0){  // 子进程
			close(fd);  // 子进程中关闭监听套接字
			ClinetHandle(newfd);  // 处理客户端
			exit(0);
		}
		else
			close(newfd);  // 父进程中关闭通信套接字
	}
	close(fd);  // 关闭监听套接字
	return 0;
}

void ClinetHandle(int newfd){
	int ret;
	char buf[BUFSIZ];  // 数据接收缓冲区
	while(1){
		bzero(buf, BUFSIZ);  // 清空缓冲区
		ret = read(newfd, buf, BUFSIZ);  // 从套接字读取数据
		if(ret < 0)
		{
			perror("read");
			exit(0);
		}
		else if(ret == 0)  // 对端关闭连接
			break;
		else
			printf("buf = %s\n", buf);  // 打印接收到的数据
	}
	close(newfd);  // 关闭与客户端的连接
}

注解:

  1. 头文件:

    • stdio.h:提供输入输出函数。
    • sys/socket.hsys/types.h:提供套接字功能支持。
    • stdlib.h:提供各种常用的库函数,如 exit
    • arpa/inet.h:提供IP地址转换函数。
    • unistd.h:提供对 POSIX 操作系统 API 的访问功能,如 readwriteclose
    • string.hstrings.h:提供字符串处理函数。
  2. 宏定义:

    • BACKLOG:监听队列中最大的连接数。
  3. 主函数 (main):

    • 解析命令行参数,确定服务器监听的 IP 地址和端口号。
    • 创建一个 TCP 套接字。
    • 设置套接字选项,使得地址(IP和端口)可以被重用,这对于服务器程序重启是很有用的。
    • 将套接字绑定到一个地址和端口。
    • 将套接字设置为监听状态,准备接受连接请求。
    • 通过 accept 函数循环等待客户端的连接请求。每当接受一个连接,就打印客户端的地址信息。
    • 使用 fork 创建一个新的进程来处理客户端请求,父进程关闭新的套接字描述符,子进程关闭监听套接字,并调用 ClinetHandle 函数来处理客户端。
  4. ClinetHandle 函数:

    • 这个函数用于处理客户端发来的数据。
    • 使用 read 函数从套接字读取数据,并将读取的数据打印出来。
    • 如果 read 函数返回 0,表示对端已经关闭了连接,因此跳出循环。
    • 使用 bzero 函数清零接收缓冲区,以便重新使用。
  5. 错误处理:

    • 程序中包含多个错误检查点,如套接字创建失败、绑定失败、监听失败等,都会输出错误信息并退出程序。

僵尸进程处理

#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>
#include <signal.h>
#include <sys/wait.h>

#define BACKLOG 5

//信号处理函数
void SigHandle(int sig){
	if(sig == SIGCHLD){
		printf("client exited\n");
		wait(NULL);
	}
}
void ClinetHandle(int newfd);
int main(int argc, char *argv[])
{
	int fd, newfd;
	struct sockaddr_in addr, clint_addr;
	socklen_t addrlen = sizeof(clint_addr);

#if 0
	struct sigaction act; 
	act.sa_handler = SigHandle;
	act.sa_flags = SA_RESTART; //不启用的话 accept函数会执行失败
	sigemptyset(&act.sa_mask);
	sigaction(SIGCHLD, &act, NULL);
#else
	signal(SIGCHLD, SigHandle);
#endif

	pid_t pid;
	
	if(argc < 3){
		fprintf(stderr, "%s<addr><port>\n", argv[0]);
		exit(0);
	}

	/*创建套接字*/
	fd = socket(AF_INET, SOCK_STREAM, 0);
	if(fd < 0){
		perror("socket");
		exit(0);
	}
	addr.sin_family = AF_INET;
	addr.sin_port = htons( atoi(argv[2]) );
	if ( inet_aton(argv[1], &addr.sin_addr) == 0) {
		fprintf(stderr, "Invalid address\n");
		exit(EXIT_FAILURE);
	}

	/*地址快速重用*/
	int flag=1,len= sizeof (int); 
	if ( setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &flag, len) == -1) { 
		      perror("setsockopt"); 
			        exit(1); 
	} 
	/*绑定通信结构体*/
	if(bind(fd, (struct sockaddr *)&addr, sizeof(addr) ) == -1){
		perror("bind");
		exit(0);
	}
	/*设置套接字为监听模式*/
	if(listen(fd, BACKLOG) == -1){
		perror("listen");
		exit(0);
	}
	while(1){
		/*接受客户端的连接请求,生成新的用于和客户端通信的套接字*/
		newfd = accept(fd, (struct sockaddr *)&clint_addr, &addrlen);
		if(newfd < 0){
			perror("accept");
			exit(0);
		}
		printf("addr:%s port:%d\n", inet_ntoa(clint_addr.sin_addr), ntohs(clint_addr.sin_port) );
		if( (pid = fork() ) < 0){
			perror("fork");
			exit(0);
		}else if(pid == 0){
			close(fd);
			ClinetHandle(newfd);
			exit(0);
		}
		else
			close(newfd);
	}
	close(fd);
	return 0;
}
void ClinetHandle(int newfd){
	int ret;
	char buf[BUFSIZ] = {};
	while(1){
		//memset(buf, 0, BUFSIZ);
		bzero(buf, BUFSIZ);
		ret = read(newfd, buf, BUFSIZ);
		if(ret < 0)
		{
			perror("read");
			exit(0);
		}
		else if(ret == 0)
			break;
		else
			printf("buf = %s\n", buf);
	}
	close(newfd);
}

TCP多线程并发

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

#define BACKLOG 5 // 最大的等待连接队列长度

void *ClinetHandle(void *arg);  // 线程函数声明

int main(int argc, char *argv[])
{
	int fd, newfd; // fd 是监听套接字,newfd 是客户端连接套接字
	struct sockaddr_in addr, clint_addr;  // 服务器和客户端的地址结构
	pthread_t tid;
	socklen_t addrlen = sizeof(clint_addr);
	
	if(argc < 3){
		fprintf(stderr, "%s<addr><port>\n", argv[0]);  // 检查命令行参数
		exit(0);
	}

	//创建套接字
	fd = socket(AF_INET, SOCK_STREAM, 0);
	if(fd < 0){
		perror("socket");
		exit(0);
	}
	addr.sin_family = AF_INET;
	addr.sin_port = htons( atoi(argv[2]));   // 设置端口号
	if ( inet_aton(argv[1], &addr.sin_addr) == 0) {   // 设置IP地址
		fprintf(stderr, "Invalid address\n");
		exit(EXIT_FAILURE);
	}

	//设置地址快速重用
	int flag=1,len= sizeof (int); 
	if ( setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &flag, len) == -1) { 
		      perror("setsockopt"); 
			        exit(1); 
	} 

	// 绑定套接字
	if(bind(fd, (struct sockaddr *)&addr, sizeof(addr) ) == -1){
		perror("bind");
		exit(0);
	}
	// 设置套接字为监听模式
	if(listen(fd, BACKLOG) == -1){
		perror("listen");
		exit(0);
	}
	while(1){
		//接受客户端的连接请求,生成新的用于和客户端通信的套接字
		newfd = accept(fd, (struct sockaddr *)&clint_addr, &addrlen);
		if(newfd < 0){
			perror("accept");
			exit(0);
		}

// 打印客户端地址信息
		printf("addr:%s port:%d\n", inet_ntoa(clint_addr.sin_addr), ntohs(clint_addr.sin_port) );
		pthread_create(&tid, NULL, ClinetHandle, &newfd);// 创建一个线程来处理客户端请求
		pthread_detach(tid); //设置分离属性,使线程结束时自动回收资源
	}
	close(fd);  // 关闭监听套接字
	return 0;
}

void *ClinetHandle(void *arg){
	int ret;
	char buf[BUFSIZ] = {};  // 接收缓冲区
	int newfd = *(int *)arg;  // 从参数中获取套接字描述符
	while(1){
		//memset(buf, 0, BUFSIZ);
		bzero(buf, BUFSIZ);    // 清空缓冲区
		ret = read(newfd, buf, BUFSIZ);    // 读取客户端数据
		if(ret < 0)
		{
			perror("read");
			exit(0);
		}
		else if(ret == 0)   // 客户端关闭连接
			break;
		else
			printf("buf = %s\n", buf);   // 输出接收到的数据
	}  
	printf("client exited\n");    // 客户端退出通知
	close(newfd);    // 关闭与客户端的连接
	return NULL;
}
  1. 多线程处理: 使用 pthread_create 创建一个新的线程来处理每个新的客户端连接。这使得服务器能够同时处理多个客户端。
  2. 线程函数 ClinetHandle: 这个函数负责从客户端读取数据并打印出来,直到客户端关闭连接。
  3. 资源管理: 使用 pthread_detach 使得线程在结束时自动释放所有资源,不需要主线程进行回收。
  4. TCP Socket 操作: 程序中使用了 socket(), bind(), listen(), 和 accept() 函数来完成 TCP 套接字的创建、绑定、监听和接受连接的基本流程。

  5. 地址重用: 使用 setsockopt() 设置 SO_REUSEADDR 选项,这允许服务器快速重启而不必等待套接字在TIME_WAIT状态消失,避免了"Address already in use"错误。

  6. 错误处理: 在关键操作后检查返回值,如套接字创建、绑定、监听等,出现错误时使用 perror() 打印错误信息并退出程序。这是处理网络编程中可能发生的各种异常情况的标准做法。

  7. 客户端信息打印: 当客户端连接成功时,服务器会打印出客户端的 IP 地址和端口号,这有助于跟踪连接到服务器的客户端。

  8. 数据读取与打印: ClinetHandle 函数中,通过循环调用 read() 从客户端套接字读取数据,并打印出来。如果 read() 返回0,意味着客户端已经关闭连接,此时跳出循环,关闭套接字,并结束线程。

  9. 线程的创建与管理: pthread_create() 被用来创建新线程,线程 ID 存储在 tid 变量中。使用 pthread_detach() 将线程设置为分离状态,这样一旦线程执行完成后,其资源会自动被系统回收。

  10. 命令行参数: 程序依赖命令行参数来获得监听的 IP 地址和端口号。如果参数数量不正确,程序会提示正确的用法并退出。

执行截图:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值