您的位置:首页 > 理论基础 > 计算机网络

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

/*
* 实现目标:
* 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;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: