UNIX高级编程【深入浅出】 网络编程 UDP/TCP

 

目录

UDP

创建 UDP 套接字

server端

client端(主动端)

示例代码

需要注意的是

UDP支持组播(群发)

示例代码(组播)

UDP支持广播

TCP

创建 TCP 套接字

server(被动端)

client(主动端)

示例代码

需要注意的是

三次握手与四次挥手

创建连接的三次握手过程

连接断开四次挥手过程

IO多路复用在TCP服务器中的应用

示例代码

UDP与TCP 协议格式​


  1. UDP

    1. SOCK_DGRAM报式套接字
      1. 全双工,无连接,不可靠(这不是缺点)
      2. 传输协议就是UDP
      3. 收发两端,通常client和server端
    2. 创建 UDP 套接字

      1. server端
        1. socket(2)创建报式套接字
        2. connect(2) / listen(2) / accept(2)都是与连接相关的,所有报式套接字都用不到
        3. 被动端,准备好接受包的条件
          1. bind(2)地址
        4. recvfrom(2)接受数据包(得到对端地址)
        5. close(2)关闭套接字
      1. client端(主动端)
        1. socket(2)创建报式套接字
        2. 主动端,第一个动作式发送数据包
          1. bind(2)可以省略的,如果省略了内核会为进程bind一个空闲的地址
        3. sendto(2)发送数据包
        4. close(2)关闭套接字
      2. server和client需要遵循相同的协议
        1. server的地址(ip+port)
        2. 数据格式(数据类型)
  2. 示例代码
    #ifndef __PORT_H__
    #define __PORT_H__
    
    #include <stdint.h>
    #define SERVERIP "x.x.x.x"
    #define SERVERPORT 1999
    #define MSGSIZE 1024
    
    #define handler_error(msg)\
    			do {perror(msg); exit(EXIT_FAILURE);} while(0)
    struct __msg{
    	int8_t id;
    	char msg[MSGSIZE];
    }__attribute__((packed));
    
    typedef struct __msg msg_t;
    
    
    
    #endif
    // Date:2023.09.14 19:51:14
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/types.h>          
    #include <sys/socket.h>
    #include <arpa/inet.h>
    #include <netinet/in.h>
    #include <netinet/ip.h>
    #include <string.h>
    #include "port.h"
    /* 客户端 */
    int main(int argc, char *argv[])
    {
    	int cnt;
    	int udp_socket;
    	struct sockaddr_in addr_in;
    	msg_t sendbuf;
    	socklen_t addr_len;
    
        if (argc < 3)
    		exit(1);
    
        // 创建报式套接子
        if(-1 == (udp_socket = socket(AF_INET, SOCK_DGRAM, 0)))
    		handler_error("socket()");
    
        // bind可省略
    
    	// 发送的数据
    	sendbuf.id = atoi(argv[1]);
    	strncpy(sendbuf.msg, argv[2], MSGSIZE);
        // server地址
        addr_in.sin_family = AF_INET;
    	addr_in.sin_port = htons(SERVERPORT);
    	addr_in.sin_addr.s_addr = inet_addr(SERVERIP);
    	//inet_aton(SERVERIP, &addr_in.sin_addr);
    
    	if(-1 == sendto(udp_socket, &sendbuf, sizeof(sendbuf.id)+strlen(sendbuf.msg)+1, 0,\
    			   	(struct sockaddr *)&addr_in, sizeof(addr_in))){
    		close(udp_socket); handler_error("sendto()");
    	}
    	memset(sendbuf.msg, '\0', MSGSIZE);
    	if(-1 == recvfrom(udp_socket, &sendbuf, sizeof(sendbuf), 0, NULL, NULL)){
    		close(udp_socket); handler_error("recvfrom()");
    	}
    
    	// 请求后得到服务器反馈
    	printf("%d , %s \n", sendbuf.id, sendbuf.msg);
    
    	close(udp_socket);
    	
    	return 0;
    }
    // Date:2023.09.14 19:51:14
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/types.h>          
    #include <sys/socket.h>
    #include <arpa/inet.h>
    #include <netinet/in.h>
    #include <netinet/ip.h>
    #include <string.h>
    #include "port.h"
    /* 服务端 */
    int main(void)
    {
    	int cnt;
    	int udp_socket;
    	struct sockaddr_in addr_in, client_addr;
    	msg_t rcvbuf;
    	socklen_t client_addr_len;
        if(-1 == (udp_socket = socket(AF_INET, SOCK_DGRAM, 0)))
    		handler_error("socket()");
        
        // 绑定地址
    	addr_in.sin_family = AF_INET;
    	addr_in.sin_port = htons(SERVERPORT);
        // 点分十进制ip转换为ip结构体
    	//addr_in.sin_addr.s_addr = inet_addr(SERVERIP);
    	inet_aton(SERVERIP, &addr_in.sin_addr);
    	if(-1 == bind(udp_socket, (struct sockaddr *)&addr_in, sizeof(addr_in))){
    		close(udp_socket); handler_error("bind()");
    	}
        // 接受client的数据包之前一定要将地址长度赋值
    	client_addr_len = sizeof(struct sockaddr_in);
    	
    	while(1){
    		// 等待接受客户端请求
    		if(-1 == (cnt = recvfrom(udp_socket, &rcvbuf, sizeof(rcvbuf), 0, (struct sockaddr *)&client_addr, &client_addr_len))){
    		close(udp_socket); handler_error("recvfrom()");
    		}
            // 调试语句
    		printf("[%s][%d] ", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
    		printf("id:%d , msg: %s\n", rcvbuf.id, rcvbuf.msg);
    
    		// 向客户端发送数据
    		rcvbuf.id = 0;
    		strcpy(rcvbuf.msg, "ok");
    		sendto(udp_socket, &rcvbuf, sizeof(rcvbuf.id) + strlen(rcvbuf.msg) + 1,0, (struct sockaddr *)&client_addr, client_addr_len);
    
    	}
    	close(udp_socket);
    	
    	return 0;
    }
    需要注意的是
    1. 在编写 UDP 程序时,你需要关注数据包的大小、分片和重组、丢包处理等问题,因为 UDP 本身不提供这些功能。此外,UDP 也不提供拥塞控制,因此在高负载或不稳定的网络环境中,可能会导致丢包和延迟增加。

    1. UDP支持组播(群发)

      1. ipv4组播地址:224~239
      2. 组播就是套接字的选项,使能组播的功能,那么就需要将套接字的组播选项打开
        1. setsockopt(2)
        2. 套接字的选项在以下手册中均有:
          1. man 7 ip(组播选项)
          2. man 7 socket
          3. man 7 udp
          4. man 7 tcp
        3. 组播选项的级别(level):IPPROTO_IP
        4. 组播选项的开关:大多数选项需要一个整型变量,如果值为1就是开启,值为0就是关闭
        5. 选项:
          1. IP_ADD_MEMBERSHIP 加入多播组
          2. IP_MULTICAST_IF 将本地设备用于多播
        6. 抓包器
          • wireshark
      3. 示例代码(组播)
#ifndef __GROUP_PROTO_H__
#define __GROUP_PROTO_H__

// 多播组 接受端口
#define GROUP_IP	"224.2.3.4"
#define RCV_PORT	1314

// 数据类型
#define MAX_MSG	1024

#endif
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <net/if.h>
#include <stdlib.h>
#include <unistd.h>
#include "group_proto.h"
/* 客户端 */ 
int main(void)
{
	int udp_socket;
	struct sockaddr_in myaddr; // man 7 ip
	struct ip_mreqn imr;
	char *buf = NULL;
	int cnt;

	udp_socket = socket(AF_INET, SOCK_DGRAM, 0);
	if (-1 == udp_socket) {
		perror("socket()");
		exit(1);
	}

	// 被动端需要bind地址
	myaddr.sin_family = AF_INET;
	myaddr.sin_port = htons(RCV_PORT);
	inet_aton("0.0.0.0"/*INADDR_ANY*/, &myaddr.sin_addr);
	if (-1 == bind(udp_socket, (struct sockaddr *)&myaddr, sizeof(myaddr))) {
		perror("bind()");
		goto ERROR1;
	}

	// 加入多播组
	inet_aton(GROUP_IP, &imr.imr_multiaddr);
	// 注意INADDR_ANY不是字符串,是uint32_t整型数
	inet_aton("0.0.0.0", &imr.imr_address);
	// 虚拟网卡的索引值,索引名转换为值if_nametoindex(3)
	imr.imr_ifindex = if_nametoindex("ens33");
	if (-1 == setsockopt(udp_socket, IPPROTO_IP, IP_ADD_MEMBERSHIP, &imr, sizeof(imr))) {
		perror("setsockopt()");
		goto ERROR1;
	}

	// 接受消息
	buf = malloc(MAX_MSG);
	// if error
	while (1) {
		memset(buf, '\0', MAX_MSG);
		cnt = recvfrom(udp_socket, buf, MAX_MSG, 0, NULL, NULL);
		if (-1 == cnt) {
			perror("recvfrom()");
			goto ERROR1;
		}
		if (strcmp(buf, "bye") == 0)
			break;
		puts(buf);
	}

	close(udp_socket);
	free(buf);
	buf = NULL;

	exit(0);
ERROR1:
	close(udp_socket);
	exit(1);
}
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <net/if.h>
#include <stdlib.h>
#include <unistd.h>
#include "group_proto.h"
/* 服务端 */
int main(void)
{
	int udp_socket;
	struct sockaddr_in snd_addr; // man 7 ip
	struct ip_mreqn imr;

	udp_socket = socket(AF_INET, SOCK_DGRAM, 0);
	if (-1 == udp_socket) {
		perror("socket()");
		exit(1);
	}

	// 将本地设备设置为多播
	inet_aton(GROUP_IP, &imr.imr_multiaddr);
	imr.imr_address.s_addr = INADDR_ANY;
	// inet_aton("0.0.0.0", &imr.imr_address);
	// 虚拟网卡的索引值,索引名转换为值if_nametoindex(3)
	imr.imr_ifindex = if_nametoindex("ens33");
	if (-1 == setsockopt(udp_socket, IPPROTO_IP, IP_MULTICAST_IF, &imr, sizeof(imr))) {
		perror("setsockopt()");
		goto ERROR1;
	}
	printf("[%d]debug...\n", __LINE__);

	// 发送消息
	snd_addr.sin_family = AF_INET;
	snd_addr.sin_port = htons(RCV_PORT);
	inet_aton(GROUP_IP, &snd_addr.sin_addr);
	while (1) {
		sendto(udp_socket, "hello", 5, 0, (struct sockaddr *)&snd_addr, sizeof(snd_addr));	
		sleep(1);
	}

	close(udp_socket);

	exit(0);
ERROR1:
	close(udp_socket);
	exit(1);
}
  1. UDP支持广播

    1. 广播选项man 7 socket
      1. SO_BROADCAST
    2. 广播选项级别:SOL_SOCKET
    3. 广播地址
      1. "255.255.255.255"全网广播
  2. TCP

    1. TCP套接字
      1. man 7 tcp
      2. 流式套接字
      3. 特点:
        1. 基于连接的,可靠传输
    2. 创建 TCP 套接字

      1. server(被动端)
        1. socket(2)创建流式套接字
        2. bind(2)地址
        3. listen(2)使套接字处于监听状态
        4. accept(2)等待接受对端的连接请求
        5. read(2) / write(2)收发消息 send(2) / recv(2)也可以
        6. close(2)关闭套接字
      2. client(主动端)
        1. socket(2)创建流式套接字
        2. connect(2)请求与服务器连接
        3. read(2) / write(2)收发消息 send(2) / recv(2)也可以
        4. close(2);
    3. 示例代码
      #ifndef __TCP_PROT_H__
      #define __TCP_PROT_H__
      
      // server地址(ip + port)
      #define SERVER_IP	"x.x.x.x"
      #define SERVER_PORT	3344
      
      // 对话格式(数据类型)
      #define MAXMSG	1024
      
      struct msg_st {
      	char msg[MAXMSG];
      }__attribute__((packed));
      
      #endif
      #include <stdio.h>
      #include <string.h>
      #include <sys/socket.h>
      #include <netinet/in.h>
      #include <netinet/ip.h>
      #include <stdlib.h>
      #include <arpa/inet.h>
      #include <unistd.h>
      
      #include "tcp_proto.h"
      /* 客户端 */
      int main(void)
      {
      	int tcp_socket;
      	struct sockaddr_in server_addr;
      	struct msg_st rcvbuf;
      	int cnt;
      
      	// 创建流式套接字
      	tcp_socket = socket(AF_INET, SOCK_STREAM, 0);
      	if (-1 == tcp_socket) {
      		perror("socket()");
      		exit(1);
      	}
      
      	// 请求与服务器连接
      	server_addr.sin_family = AF_INET;
      	server_addr.sin_port = htons(SERVER_PORT);
      	inet_aton(SERVER_IP, &server_addr.sin_addr);
      	if (-1 == connect(tcp_socket, (struct sockaddr *)&server_addr, sizeof(server_addr))) {
      		perror("connect()");
      		goto ERROR;
      	}
      
      	// 接受
      	memset(rcvbuf.msg, '\0', MAXMSG);
      	cnt = read(tcp_socket, &rcvbuf, MAXMSG);
      	printf("from server rcv msg:%s with %dbytes\n", rcvbuf.msg, cnt);
      
      	close(tcp_socket);
      
      	exit(0);
      ERROR:
      	close(tcp_socket);
      	exit(1);
      }
      
      #include <stdio.h>
      #include <sys/socket.h>
      #include <netinet/in.h>
      #include <netinet/ip.h>
      #include <stdlib.h>
      #include <arpa/inet.h>
      #include <unistd.h>
      
      #include "tcp_proto.h"
      /* 服务端 */
      int main(void)
      {
      	int tcp_socket, accepted_socket;
      	struct sockaddr_in myaddr;
      	pid_t pid;
      
      	// 创建流式套接字
      	tcp_socket = socket(AF_INET, SOCK_STREAM, 0);
      	if (-1 == tcp_socket) {
      		perror("socket()");
      		exit(1);
      	}
      
      	// 绑定本地地址
      	myaddr.sin_family = AF_INET;
      	myaddr.sin_port = htons(SERVER_PORT);
      	myaddr.sin_addr.s_addr = INADDR_ANY;
      	if (-1 == bind(tcp_socket, (struct sockaddr *)&myaddr, sizeof(myaddr))) {
      		perror("bind()");
      		goto ERROR;
      	}
      
      	// 套接字处于监听状态
      	if (-1 == listen(tcp_socket, 200)) {
      		perror("listen()");
      		goto ERROR;
      	}
      
      	// sigaction(SIGCHLD, ); SA_NOCHLDWAIT
      
      	while (1) {
      		// 等待接受客户端的连接请求
      		accepted_socket = accept(tcp_socket, NULL, NULL);
      		if (-1 == accepted_socket) {
      			perror("accept()");
      			goto ERROR;
      		}
      		// 成功后accepted_socket就是用于数据交换的套接字 原来的tcp_socket是用于继续接受客户端连接请求的
      		pid = fork();
      		if (-1 == pid) {
      			perror("fork()");
      			goto ERROR;
      		}
      		if (0 == pid) {
      			// 数据交换 用accepted_socket
      			close(tcp_socket);
      			sleep(1);
      			write(accepted_socket, "can you see that?", 17);
      			close(accepted_socket);
      			exit(0);
      		}
      		close(accepted_socket);
      	}
      
      	close(tcp_socket);
      
      	exit(0);
      ERROR:
      	close(tcp_socket);
      	exit(1);
      }
      
  3. 需要注意的是
    1. TCP 提供了可靠的数据传输,包括数据的分段、重组、流量控制和拥塞控制机制。它确保数据按照正确的顺序到达目标,并通过确认机制检测和恢复丢失或损坏的数据。TCP 还实现了流量控制,以避免发送方过载和接收方缓冲区溢出,以及拥塞控制,以优化网络性能和公平共享带宽。
    2. TCP 提供了可靠的、面向连接的数据传输,适用于对数据完整性和可靠性要求较高的应用场景,如文件传输、网页浏览、电子邮件等
  4. 三次握手与四次挥手

    1. 创建连接的三次握手过程
      1. client向server发起连接请求(第一次握手)
        1. SYN置1, seq=x
      2. server收到了client发送的数据包后,会向client发送一个应答,同时发送一个序号(第二次握手)
        1. ACK=1, ack=x+1, seq=y
      3. client收到server的应答后,向server发送最后的应答(第三次握手)
        1. ACK=1, ack = y+1
      4. 即客户端发送连接请求、服务器响应连接请求并发送确认、客户端再次响应确认
  5. 连接断开四次挥手过程
    1. client向server发送请求断开的数据包(第一次挥手)
      1. FIN = 1, seq = m
    2. server收到了client请求断开的数据包后,会向client端发送应答(第二次挥手)
      1. ACK = 1, ack = m + 1
    3. 当server数据交换完成,也不需要通讯的时候,向client发送断开数据包(第三次挥手)
      1. FIN = 1,seq = n
    4. client最后向server发送应答(第四次挥手)
      1. ACK = 1, ack = n + 1
    5. 即客户端发送关闭连接请求、服务器确认关闭连接请求、服务器发送关闭连接请求、客户端确认关闭连接请求并关闭连接
  6. IO多路复用在TCP服务器中的应用

    1. tcp的服务器不是盲目的接收到链接就并发,而是使用select/poll监听到已连接的套接字可读再并发
    2. 对于套接字文件,有链接请求和有数据到达都是可读事件
    3. 当client端close套接字,则server端的套接字也是发生了可读事件,并且读到的数据是0.
  7. 示例代码
    #ifndef __TCP_PROT_H__
    #define __TCP_PROT_H__
    
    // server地址(ip + port)
    #define SERVER_IP	"x.x.x.x"
    #define SERVER_PORT	3344
    
    // 对话格式(数据类型)
    #define MAXMSG	1024
    
    struct msg_st {
    	char msg[MAXMSG];
    }__attribute__((packed));
    
    #endif
    
    #include <stdio.h>
    #include <string.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <netinet/ip.h>
    #include <stdlib.h>
    #include <arpa/inet.h>
    #include <unistd.h>
    
    #include "tcp_proto.h"
    /* 客户端 */
    int main(void)
    {
    	int tcp_socket;
    	struct sockaddr_in server_addr;
    	struct msg_st sndbuf;
    	int cnt;
    
    	// 创建流式套接字
    	tcp_socket = socket(AF_INET, SOCK_STREAM, 0);
    	if (-1 == tcp_socket) {
    		perror("socket()");
    		exit(1);
    	}
    
    	// 请求与服务器连接
    	server_addr.sin_family = AF_INET;
    	server_addr.sin_port = htons(SERVER_PORT);
    	inet_aton(SERVER_IP, &server_addr.sin_addr);
    	if (-1 == connect(tcp_socket, (struct sockaddr *)&server_addr, sizeof(server_addr))) {
    		perror("connect()");
    		goto ERROR;
    	}
    
    	// 接受
    	memset(sndbuf.msg, '\0', MAXMSG);
    	strncpy(sndbuf.msg, "this is a test", MAXMSG);
    	write(tcp_socket, &sndbuf, MAXMSG);
    
    	sleep(2);
    
    	close(tcp_socket);
    
    	exit(0);
    ERROR:
    	close(tcp_socket);
    	exit(1);
    }
    
    #include <stdio.h>
    #include <errno.h>
    #include <string.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <netinet/ip.h>
    #include <stdlib.h>
    #include <arpa/inet.h>
    #include <unistd.h>
    #include <poll.h>
    #include "tcp_proto.h"
    /* 服务端 */
    int main(void)
    {
    	int tcp_socket, accepted_socket;
    	struct sockaddr_in myaddr;
    	pid_t pid;
    	struct pollfd *pfd = NULL;
    	int nfds = 0;
    	struct msg_st rcvbuf;
    	int i;
    	int cnt;
    
    	// 创建流式套接字
    	tcp_socket = socket(AF_INET, SOCK_STREAM, 0);
    	if (-1 == tcp_socket) {
    		perror("socket()");
    		exit(1);
    	}
    
    	// 绑定本地地址
    	myaddr.sin_family = AF_INET;
    	myaddr.sin_port = htons(SERVER_PORT);
    	myaddr.sin_addr.s_addr = INADDR_ANY;
    	if (-1 == bind(tcp_socket, (struct sockaddr *)&myaddr, sizeof(myaddr))) {
    		perror("bind()");
    		goto ERROR;
    	}
    
    	// 套接字处于监听状态
    	if (-1 == listen(tcp_socket, 200)) {
    		perror("listen()");
    		goto ERROR;
    	}
    
    	// 监听tcp_socket
    	pfd = calloc(1, sizeof(struct pollfd));
    	if (NULL == pfd) {
    		perror("calloc()");
    		goto ERROR;
    	}
    	pfd[0].fd = tcp_socket;
    	pfd[0].events = POLLIN; // tcp_socket是否有连接 
    	nfds = 1;
    
    	while (1) {
    		if (-1 == poll(pfd, nfds, -1)) {
    			if (errno == EINTR)
    				continue; // 被信号打断
    			perror("poll()");
    			goto ERROR;
    		}
    		// 监听的事件发生了
    		for (i = 0; i < nfds; i++) {
    			if (i == 0) {
    				if (pfd[0].fd == tcp_socket && pfd[0].revents & POLLIN) {
    					// 套接字有连接到来了
    					accepted_socket = accept(tcp_socket, NULL, NULL);
    					if (-1 == accepted_socket) {
    						perror("accept()");
    						goto ERROR;
    					}
    					printf("**********client connected succefully************\n");
    					// accepted_socket 就是将来用于数据交换的套接字
    					// 如果可读了再读
    					pfd = realloc(pfd, (nfds + 1) * sizeof(struct pollfd));
    					// if error
    					pfd[nfds].fd = accepted_socket;
    					pfd[nfds].events = POLLIN; // 数据
    					pfd[nfds].revents = 0;
    					nfds ++;
    				}
    			} else {
    				// 不是客户端的连接请求,而是有数据请求
    				if (pfd[i].revents & POLLIN) {
    					// 创建子进程/线程
    					memset(rcvbuf.msg, '\0', MAXMSG);
    					cnt = recv(pfd[i].fd, &rcvbuf, MAXMSG, 0);
    					if (cnt == -1) {
    						perror("recv()");
    						goto ERROR;
    					}
    					if (cnt == 0) {
    						// c端close()
    						printf("***************888***************\n");
    						close(pfd[i].fd);
    						pfd[i].events = 0;
    						memmove(pfd + i, pfd + i + 1, (nfds - (i + 1)) * sizeof(struct pollfd));
    						nfds--;
    						pfd = realloc(pfd, nfds * sizeof(struct pollfd));
    						i--; // 下一次循环要执行i++
    					} else {
    						printf("!!!!!!!!!!!recv data!!!!!!!!!!!\n");
    						// 接受到数据请求,处理请求
    						// 暂且调试输出
    						puts(rcvbuf.msg);
    					}
    				}
    			}
    		}
    	}
    
    	close(tcp_socket);
    
    	exit(0);
    ERROR:
    	close(tcp_socket);
    	exit(1);
    }
    

UDP与TCP 协议格式

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值