Linux TCP通信详解&UDP聊天室(6.9)
2015-06-11 20:07
573 查看
[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
[3] UDP聊天室
1. 简单聊天室
实现服务端到每个客户端的广播
见《5. UDP聊天室目录》
2. 规范
(1) 需求分析
登录
聊天
退出
(2) 协议制定
登录: 功能号 用户名
1 "wangjie"
聊天: 功能号 内容
退出: 功能号
(3) 协议实现方案
1. 结构体
对齐: 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 位操作
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; }
相关文章推荐
- 探索 OpenStack 之(8):Neutron 深入探索之 OVS + GRE 之 完整网络流程 篇(转)
- HttpUrlConnection retryPost 重发
- 基于linux-2.6.35的网络视频服务器移植
- loadrunner脚本编写http协议
- 实验五 TCP传输及加密
- 简约之美Jodd-http--深入源码理解http协议
- 黑马程序员-网络编程
- 网络流24题 -No.18 分配问题
- Android 网络通信框架Volley简介(Google IO 2013)
- 网络流24题 -No.17 运输问题
- 解决Apache HttpClient Cookie rejected的问题
- 封装音视频流为RTP包来网络传输
- HTTP协议之响应头Date与Age
- IOS学习之网络笔记
- HttpURLConnection
- httpclient https忽略证书直接请求
- TCP 通信过程中各步骤的状态
- http代理之HTTP协议详解
- 获取路由器网络信息(mac,ip,router,dns,dhcp clients)
- 进度计划管理软件 PowerPlan (包含GRID,甘特图,直方图,网络图,跟踪逻辑,时标概要图等功能)