Linux TCP通信详解&UDP聊天室(6.9)

[1] TCP通信原理(见"4.tcp"目录)
    TCP通信分服务端和客户端:
    服务端: 建立服务,等待客户端连接,响应客户端的请求
    客户端: 连接服务端,请求服务端
    具体原理如下:
    1. 建立连接
      《三次握手.bmp》
       
    2. 断开连接
      《四次挥手.bmp》
    
    3. 运行(服务端如何建立?客户端如何连接?主要是流程及背后原理)
      《运行原理.bmp》
      注意: 1. 监听套接字(套接字被设置成监听模式,专门接受客户端连接请求)
            2. 流程每一步的作用
       
    4. 数据传输
      《数据传输原理.bmp》
      建立连接后,好像在服务器套接字(跟客户端连接的套接字)和客户端套接字之间建立了两个管道,用于
      数据的发送和接收,所以客户端套接字或服务端套接字(跟客户端连接的套接字)都有两个缓冲区--接收缓冲区和发送缓冲区
      
[2] TCP通信实现
    1. 头文件
       跟UDP相同
       
    2. 数据结构
       跟UDP相同
       
    3. 函数
       服务器实现流程:
       1. 创建套接字(用于监听)          买手机
          sockfd = socket(AF_INET, SOCK_STREAM, 0);
          
       2. 绑定服务端地址(ip和port)      插入SIM
          ret = bind(sockfd, ..., ...);
          
       3. 设置监听模式                  开机
          /*
           * @param[in] sockfd 需要设置为监听模式的套接字
           * @param[in] backlog 最大的等待连接个数
           * @return @li 0 设置成功
           * @li -1 设置失败(错误码见errno)
           */
          int listen(int sockfd, int backlog);
          
       4. 接收并建立和客户端连接(建立新套接字)  接听
          /*
           * @param[in] sockfd 监听套接字
           * @param[out] addr 客户端的地址(ip和port)
           * @param[in] addrlen addr地址缓冲区的字节数
           * @param[out] addrlen 实际的地址大小
           * @return @li >= 0 新建的套接字(跟客户端连接的套接字)
           * @li -1 设置失败(错误码见errno)
           * @notes 监听队列为空时, accept默认会阻塞
           */
          int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
          
       5. 接收数据                      通话
          /*
           * @param[in] sockfd 跟客户端连接的套接字
           * @param[out] buf 接收数据的缓冲
           * @param[in] len 希望接收的字节数
           * @param[in] flags 0
           * @return 实际接收到的字节数
           * @li -1 设置失败(错误码见errno)
           * @li 0 对端关闭套件字
           * @notes 接收缓冲区为空时, recv默认会阻塞
           * read(sockfd, buf, len) <----> recv(sockfd, buf, len, 0);
           */
          ssize_t recv(int sockfd, void *buf, size_t len, int flags);
          
       6. 发送数据                      通话
          /*
           * @param[in] sockfd 跟客户端连接的套接字
           * @param[in] buf 发送数据的缓冲
           * @param[in] len 发送的字节数
           * @param[in] flags 0
           * @return 实际发送的字节数
           * @li -1 设置失败(错误码见errno)
           * @notes 发送缓冲区满时, send默认会阻塞
           * write(sockfd, buf, len) <----> send(sockfd, buf, len, 0);
           */
          ssize_t send(int sockfd, const void *buf, size_t len, int flags);
       
       7. 关闭新套接字(和客户端连接的)  挂机
          close
          
       8. 关闭监听套接字                关机
          close
       
       客户端实现流程:
       1. 创建套接字                    买手机
          sockfd = socket(AF_INET, SOCK_STREAM, 0);
          
       2. 连接服务器                    插入SIM、开机和拨打电话
          /*
           * @param[in] sockfd 套接字
           * @param[in] addr 服务器的地址(ip和port)
           * @param[in] addrlen addr地址的字节数
           * @return @li 0 连接成功
           * @li -1 连接失败(错误码见errno)
           * @notes 连接建立过程中, connect默认会阻塞
           */
          int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
          
       3. 发送数据                      通话
          send/write
          
       4. 接收数据                      通话
          recv/read
          
       5. 关闭套接字                    关机
          close

/*
 * 实现目标:
 * tcp服务端
 *
 * 实现步骤:
 * 1. socket(listen)
 * 2. bind(ip 和 port)
 * 3. listen
 * 4. accept(新创建socket--和客户端连接的)
 * 5. recv客户数据
 * 6. send客户端发送过来的数据
 * 7. close新创建socket
 * 8. close监听套接字
 */
 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <errno.h>

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

// ./server 192.168.0.249 8888
int main(int argc, const char *argv[])
{
	int ret;
	int sockfd;
	int clientfd;
	struct sockaddr_in server_addr;
	struct sockaddr_in peer_addr;
	socklen_t addrlen = sizeof(peer_addr);
	char packet[1024];
	
	if (argc < 3){
		fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
		exit(EXIT_FAILURE);
	}
	
	// 1. socket(listen)
	sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (-1 == sockfd){
		perror("Fail to socket.");
		exit(EXIT_FAILURE);
	}
	
	// 2. bind(ip 和 port)
	bzero(&server_addr, sizeof(server_addr));
	server_addr.sin_family = AF_INET;
	server_addr.sin_port = htons(atoi(argv[2]));
	server_addr.sin_addr.s_addr = inet_addr(argv[1]);
	
	ret = bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
	if (-1 == ret){
		perror("Fail to bind.");
		exit(EXIT_FAILURE);
	}
	
	// 3. listen
	ret = listen(sockfd, 10);
	if (-1 == ret){
		perror("Fail to listen.");
		exit(EXIT_FAILURE);
	}
	
	while (1) {
		// 4. accept(新创建socket--和客户端连接的)
		clientfd = accept(sockfd, (struct sockaddr *)&peer_addr, &addrlen);
		if (-1 == clientfd){
			perror("Fail to accept.");
			break;
		}
		
		printf("---------------------------------------\n");
		printf("ip   : %s\n", inet_ntoa(peer_addr.sin_addr));
		printf("port : %d\n", ntohs(peer_addr.sin_port));
		printf("---------------------------------------\n");
	
	// 5. recv客户数据
		ret = recv(clientfd, packet, sizeof(packet), 0);
		if (-1 == ret){
			perror("Fail to recv.");
			close(clientfd);
			break;
		}
		packet[ret] = '\0';
		printf("recv : %s\n", packet);
		
	// 6. send客户端发送过来的数据
		ret = send(clientfd, packet, ret, 0);
		if (-1 == ret){
			perror("Fail to send.");
			close(clientfd);
			break;
		}
		
	// 7. close新创建socket
		close(clientfd);
		
		if (strcmp(packet, "bye") == 0) break;
	}
	
	// 	8. close监听套接字
	close(sockfd);
	
	return 0;
}

/*
 * 实现目标:
 * tcp客户端
 *
 * 实现步骤:
 * 1. socket
 * 2. 接收用户输入
 * 3. connect(服务端)
 * 4. send用户输入的数据到客户端
 * 5. recv服务端发送过来的数据, 显示
 * 6. close套接字
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <errno.h>

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

// ./server 192.168.0.249 8888
int main(int argc, const char *argv[])
{
	int ret;
	int sockfd;
	struct sockaddr_in server_addr;
	char packet[1024];
	
	if (argc < 3){
		fprintf(stderr, "Usage: %s <server ip> <server port>\n", argv[0]);
		exit(EXIT_FAILURE);
	}
	

	while (1) {
	// 1. socket
		sockfd = socket(AF_INET, SOCK_STREAM, 0);
		if (-1 == sockfd){
			perror("Fail to socket.");
			break;
		}
	
	// 2. 接收用户输入
		putchar('>');
		fgets(packet, sizeof(packet), stdin);
		packet[strlen(packet) - 1] = '\0';
	
	// 3. connect(服务端)
		bzero(&server_addr, sizeof(server_addr));
		server_addr.sin_family = AF_INET;
		server_addr.sin_port = htons(atoi(argv[2]));
		server_addr.sin_addr.s_addr = inet_addr(argv[1]);
		
		ret = connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
		if (-1 == ret){
			perror("Fail to connect.");
			close(sockfd);
			break;
		}
		
	// 4. send用户输入的数据到客户端
		ret = send(sockfd, packet, strlen(packet), 0);
		if (-1 == ret){
			perror("Fail to send.");
			close(sockfd);
			break;
		}
		
 	// 5. recv服务端发送过来的数据, 显示
 		ret = recv(sockfd, packet, sizeof(packet), 0);
 		if (-1 == ret){
 			perror("Fail to recv.");
			close(sockfd);
			break;
 		}
 		packet[ret] = '\0';
 		printf("recv : %s\n", packet);
 		
	// 6. close套接字
		close(sockfd);
		
		if (strcmp(packet, "bye") == 0) break;
	}
	
	
	return 0;
}

          
[3] UDP聊天室
    1. 简单聊天室
       实现服务端到每个客户端的广播
       见《5. UDP聊天室目录》
       
    2. 规范
       (1) 需求分析
           登录
           聊天
           退出
           
       (2) 协议制定
           登录: 功能号 用户名
                   1    "wangjie"
                 
           聊天: 功能号 内容
           退出: 功能号
           
       (3) 协议实现方案
           1. 结构体

              //32机器:
              typedef struct {
              	char buf[1020];
              	空一个字节
              	int funcno;
              }
              
              //64位机器(int 64)
              typedef struct {
              	char buf[1020];
              	//空一个字节            实际上,本结构体会自动空4字节
              	int funcno;       
              }
              概念:
              对齐: char类型的数据要求从能被1整除的位置开始存放
                    short类型的数据要求从能被2整除的位置开始存放
                    int类型的数据要求从能被4整除的位置开始存放
                    long long类型的数据要求从能被8整除的位置开始存放
                    编译器,会自动让每个变量对齐,但是变量中的成员,为了对齐,成员之间会有空白内存
              
                              客户端       服务器        错误原因                  解决
              字节序          小端         大端          两边的数据存放顺序不同    统一使用大端(网络字节序), htonl/htons ntohl/ntohs
              对齐(int)       16            32           结构体成员之间会有填充    确保机器的位数和编译器对基本数据类型的理解一样(不太可能实现)
                                                         填充的字节数不一定相同    设计好结构体,确保机器位数和对齐不影响数据结构的理解
              位数(计算机)    8             32
           
           2. 按字节顺序存放
              将所有数据转换成字符串,然后按顺序存放
              127 ---> "127"
              2.7 ---> "2.7"
              
              按字节顺序存放(int 32)
              32---大端---> 4个8bit                  位操作
              4个8bit---大端-->32bit                 位操作
#ifndef	__CLIENT_H__
#define __CLIENT_H__
#define	LEN_MAX_NAME	20

// 2. 定义结构体
typedef struct client_t{
	char name[LEN_MAX_NAME + 1];
	char ip[16];
	unsigned short port;
	
	struct client_t *next;
} CLIENT;

void client_link_init(CLENT *head);
int client_link_add(CLENT *head, const char *name, const char *ip, unsigned short port);
void client_link_del(CLENT *head, const char *ip, unsigned short port);
int client_link_get_for_index(CLENT *head, int index, char *name, char *ip, unsigned short *port);
void client_link_destroy(CLENT *head);

#endif // __CLIENT_H__

/*
 * 实现目标:
 * 实现链表
 *
 * 实现步骤:
 * 1. 规划链表结构
 *    保存客户端的信息(用户名、ip 和 port)
 *    
 * 2. 定义结构体
 *    见头文件
 *
 * 3. 实现链表操作
 *    3.1 初始化链表
 *    3.2 添加客户端到链表
 *    3.3 客户端(从链表中)删除
 *    3.4 依次取出每个客户端
 *    3.5 销毁链表
 */

// CLENT head;
// client_link_init(&head);
// client_link_add(&head, "", "...", 8888);

// 3.1 初始化链表
void client_link_init(CLENT *head)
{
	head->name[0] = '\0';
	head->ip[0] = '\0';
	head->port = 0;
	head->next = NULL;
}

// 3.2 添加客户端到链表
int client_link_add(CLENT *head, const char *name, const char *ip, unsigned short port)
{
	CLENT *p, *q;
	CLENT *pclient;

	p = head;
	while (p != NULL){
		if (strcmp(p->ip, ip) == 0 && p->port == port){
			return 0;
		}
		
		q = p;
		p = p->next;
	}
	
	pclient = (CLENT *)malloc(sizeof(CLENT));
	if (NULL == pclient) {
		return -1;
	}
	strcpy(pclient->name, name);
	strcpy(pclient->ip, ip);
	pclient->port = port;
	pclient->next = NULL;
	
	q->next = pclient;
	
	printf("--------------------------------------------\n");
	printf("name : %s\n", name);
	printf("client(%s : %d)\n", ip, (int)port);
	printf("--------------------------------------------\n");
	
	return 0;
}

// 3.3 客户端(从链表中)删除
void client_link_del(CLENT *head, const char *ip, unsigned short port)
{
	CLENT *p, *q;

	p = head;
	while (p != NULL){
		if (strcmp(p->ip, ip) == 0 && p->port == port){
			break;
		}
		
		q = p;
		p = p->next;
	}
	
	// 没找到
	if (NULL == p){
		return;
	} 
	
	// 找到
	q->next = p->next;
	
	printf("--------------------------------------------\n");
	printf("name : %s\n", p->name);
	printf("client(%s : %d)\n", p->ip, (int)p->port);
	printf("--------------------------------------------\n");
	
	free(p);
	
}

// 3.4 依次取出每个客户端
int client_link_get_for_index(CLENT *head, int index, char *name, char *ip, unsigned short *port)
{
	int i = 0;
	CLENT *p = head->next;
	
	while (p != NULL){
		if (i == index){
			strcpy(name, p->name);
			strcpy(ip, p->ip);
			*port = p->port;
			return 1;
		}
		
		p = p->next;
		i++;
	}
	
	return 0;
}

//  3.5 销毁链表
void client_link_destroy(CLENT *head)
{
	CLENT *p = head->next;
	
	while (p != NULL){
		head->next = p->next;
		free(p);
		p = head->next;
	}
}

/*
 * 实现目标:
 * 简单的UDP聊天室--服务端
 *
 * 实现步骤:
 * 1. 客户端管理
 *    1.1 实现链表操作
 *        见《client_link.c》
 *    1.2 创建链表(使用链表保存客户端信息)
 *    1.3 添加客户端到链表(客户端第一次发数据上来)
 *    1.4 收到"bye",将对应客户端(从链表中)删除
 *
 * 2. 实现向客户端的广播
 *    2.1 依次取出客户端信息
 *    2.2 发送消息
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

// bzero
#include <strings.h>

// net
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

// ./server 192.168.0.249 8888
int main(int argc, const char *argv[])
{
	if (argc < 3){
		fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
		exit(EXIT_FAILURE);
	}
	
	return 0;
}

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页