再谈“协议”
1. 基本概念
我们知道,协议就是一种“约定”。套接字实现的网络通信的接口,在读写数据时,都是按照“字符串”的方式来发送和接收的,那如果我们要传输一些“结构化的数据”,该怎么办呢?
这里我们就要提出两个概念:
(1)序列化:将对象的状态信息转化为可以存储或是可以传输的形式的过程,比如将结构体等类型转化为字符串类型;
(2)反序列化:将可以存储或是可以传输的形式转化为对象的状态信息的过程,比如将字符串类型转化为结构体等类型。
2. 具体实现
比如,我们要实现一个网络版的加法计算器,有两种方法:
(1)客户端向服务器端发送一个形容“1+1”的字符串,该字符串以“+”为分隔,有两个操作数,且两个操作数都是整型,“+”即为运算符,且只能是“+”,同时数字与运算符间不能有空格。
(2)我们可以定义两个结构体来表示我们要交互的信息,一个用于传输要计算的数据,一个用于接收计算后的结果。发送数据时通过序列化将数据转换为一个字符串,接收时通过反序列化再将字符串转回结构体,具体实现代码如下:
1)要用到的头文件,以及结构体的定义:
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MAXSIZE 128
typedef struct
{//客服端发送的用于计算的两个数据
int x;
int y;
}request;
typedef struct
{//服务器端计算完返回给客户端的值
int res;
}response;
2)服务器端——计算两个数的和
//实现网络版加法计算器,利用TCP多线程服务器计算客户端发过来数的和
#include "comm.h"
typedef struct
{
int sock;
char ip[24];
int port;
}net_info_t;
int startup(int port, char* ip)
{
//1.创建套接字,这里是流式的套接字,因为TCP面向字节流
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0)
{
printf("socket error\n");
exit(2);//套接字创建失败,直接终止进程,因为没有套接字网络通信根本无法实现,后续代码根本不用执行
}
//2.绑定
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);//端口号
local.sin_addr.s_addr = inet_addr(ip);//IP
if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0)
{
printf("bind error\n");
exit(3);
}
//3.监听:一直等待客户来连接(因为TCP是面向连接的),提取新连接;
if(listen(sock, 5) < 0)//最多容纳5个客户连接请求
{
printf("listen error\n");
exit(4);
}
return sock;//返回一个监听套接字
}
void service(int sock, char* ip, int port)
{
while(1)
{
request r;
//read函数可以读取任意类型的内容
ssize_t s = read(sock, &r, sizeof(r));
response rp;
rp.res = r.x + r.y;
write(sock, &rp, sizeof(rp));
}
}
void* thread_service(void* arg)
{
net_info_t *p = (net_info_t* )arg;
service(p->sock, p->ip, p->port);
close(p->sock);
free(p);
}
//./tcp_server 127.0.0.1 8080
int main(int argc, char* argv[])
{
if(argc != 3)
{
printf("Usage: %s [ip] [port]\n", argv[0]);
exit(1);
}
//创建套接字
int listen_sock = startup(atoi(argv[2]), argv[1]);
struct sockaddr_in peer;
char ipbuf[24];
for( ; ; )
{
ipbuf[0] = 0;
socklen_t len = sizeof(peer);
//从监听套接字中拿连接
int new_sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
if(new_sock < 0)//拿连接失败,不用管,因为还可以去拿其他连接
{
printf("accept error\n");
continue;
}
//获得了一个新连接
inet_ntop(AF_INET, (const void*)&peer.sin_addr, ipbuf, sizeof(ipbuf));//将四字节IP地址转换为点分十进制
int p = ntohs(peer.sin_port);//端口号:网络序列转换为主机序列的端口号
printf("get a new connect,[%s:%d]\n", ipbuf, p);//将新连接的IP和端口号打印
//这里不用像多进程的版本关闭多余的文件描述符
//因为线程共享地址空间,关掉一个文件,其他线程就看不到用不了了
net_info_t* info = (net_info_t*)malloc(sizeof(net_info_t));
if(info == NULL)
{
perror("malloc");
close(new_sock);
continue;
}
info->sock = new_sock;
strcpy(info->ip,ipbuf);
info->port = p;
pthread_t tid;
pthread_create(&tid, NULL, thread_service, (void* )info);
pthread_detach(tid);//线程分离后,该线程的资源会自动释放
}
return 0;
}
3)客户端——发送要计算的数据,以及接收计算结果
//客户端,输入两个用于加法计算的数
#include "comm.h"
//./tcp_client 127.0.0.1 8080
int main(int argc, char* argv[])
{
if(argc != 3)
{
printf("Usage: %s [ip] [port]\n", argv[0]);
return 1;
}
//创建套接字
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0)
{
printf("socket error\n");
return 2;
}
//客户端不用绑定(bind),不用监听(listen),不用获取新连接(accept)
//客户端有一个个性化操作connect,向服务器发起连接
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);
if(connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0)
{
printf("connect error\n");
return 3;
}
//走到这,连接成功
//发送数据,并收回结果
char buf[MAXSIZE];
while(1)
{
request r;
printf("please enter# ");
fflush(stdout);
scanf("%d%d", &r.x, &r.y);
write(sock, &r, sizeof(r));
response rp;
read(sock, &rp, sizeof(rp));
printf("%d+%d = %d\n", r.x, r.y, rp.res);
}
close(sock);
return 0;
}
4)运行结果:
因为上述代码实现的是多线程版本,所以可以接受多个连接请求,具体测试结果如下: