说明:(三次握手)
1.2 基本UDP客户—服务器程序设计基本框架
Socket接口是TCP/IP网络的API,Socket接口定义了许多函数或例程,程序员可以用它们来开发TCP/IP网络上的应用程序。要学Internet上的TCP/IP网络编程,必须理解Socket接口。
Socket接口设计者最先是将接口放在Unix操作系统里面的。如果了解Unix系统的输入和输出的话,就很容易了解Socket了。网络的Socket数据传输是一种特殊的I/O,Socket也是一种文件描述符。Socket也具有一个类似于打开文件的函数调用Socket(),该函数返回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现的。常用的Socket类型有两种:流式Socket(SOCK_STREAM)和数据报式Socket(SOCK_DGRAM)。流式是一种面向连接的Socket,针对于面向连接的TCP服务应用;数据报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用。
socket调用库函数主要有:
1.创建套接字 socket(af,type,protocol) 2.建立地址和套接字的联系 bind(sockid, local addr, addrlen) 3.服务器端侦听客户端的请求 listen( Sockid ,quenlen) 4.建立服务器/客户端的连接 (面向连接TCP) 客户端请求连接 connect(sockid, destaddr, addrlen) 服务器端等待从编号为Sockid的Socket上接收客户连接请求 newsockid=accept(Sockid,Clientaddr, paddrlen) 5.发送/接收数据 面向连接: send(sockid, buff, bufflen) recv( ) 面向无连接: sendto(sockid,buff,…,addrlen) recvfrom( ) 6.释放套接字 close(sockid)
int socket(int domain, int type, int protocol);
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
我们的程序中对 myaddr 参数是这样初始化的:
int listen(int sockfd, int backlog);
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
我们的服务器程序结构是这样的:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
服务器的工作流程:首先调用socket函数创建一个Socket,然后调用bind函数将其与本机地址以及一个本地端口号绑定,然后调用listen在相应的socket上监听,当accpet接收到一个连接服务请求时,将生成一个新的socket。服务器显示该客户机的IP地址,并通过新的socket向客户端发送字符串" hi,I am server!"。最后关闭该socket
int main()
{
int sock_fd,client_fd; /*sock_fd:监听socket;client_fd:数据传输socket */
struct sockaddr_in ser_addr; /* 本机地址信息 */
struct sockaddr_in cli_addr; /* 客户端地址信息 */
char msg[MAX_MSG_SIZE];/* 缓冲区*/
ser_sockfd=socket(AF_INET,SOCK_STREAM,0);/*创建连接的SOCKET */
if(ser_sockfd<0)
{/*创建失败 */
fprintf(stderr,"socker Error:%s\n",strerror(errno));
exit(1);
}
/* 初始化服务器地址*/
addrlen=sizeof(struct sockaddr_in);
bzero(&ser_addr,addrlen);
ser_addr.sin_family=AF_INET;
ser_addr.sin_addr.s_addr=htonl(INADDR_ANY);
ser_addr.sin_port=htons(SERVER_PORT);
if(bind(ser_sockfd,(struct sockaddr*)&ser_addr,sizeof(struct sockaddr_in))<0)
{ /*绑定失败 */
fprintf(stderr,"Bind Error:%s\n",strerror(errno));
exit(1);
}
/*侦听客户端请求*/
if(listen(ser_sockfd,BACKLOG)<0)
{
fprintf(stderr,"Listen Error:%s\n",strerror(errno));
close(ser_sockfd);
exit(1);
}
while(1)
{/* 等待接收客户连接请求*/
cli_sockfd=accept(ser_sockfd,(struct sockaddr*) & cli_addr,&addrlen);
if(cli_sockfd<=0)
{
fprintf(stderr,"Accept Error:%s\n",strerror(errno));
}
else
{/*开始服务*/
recv(cli_addr,msg,MAX_MSG_SIZE,0); /* 接受数据*/
printf("received a connection from %sn",inet_ntoa(cli_addr.sin_addr));
printf("%s\n",msg);/*在屏幕上打印出来 */
strcpy(msg,"hi,I am server!");
send(cli_addr,msg,sizeof(msg),0); /*发送的数据*/
close(cli_addr);
}
}
close(ser_sockfd);
}
客户端的工作流程:首先调用socket函数创建一个Socket,然后初始化服务器地址结构,调用connect函数请求连接服务器,通过新的socket向客户端发送字符串" hi,I am client!"。最后关闭该socket。
int main()
{
int cli_sockfd;/*客户端SOCKET */
int addrlen;
char seraddr[14];
struct sockaddr_in ser_addr,/* 服务器的地址*/
char msg[MAX_MSG_SIZE];/* 缓冲区*/
GetServerAddr(seraddr);
cli_sockfd=socket(AF_INET,SOCK_STREAM,0);/*创建连接的SOCKET */
if(ser_sockfd<0)
{/*创建失败 */
fprintf(stderr,"socker Error:%s\n",strerror(errno));
exit(1);
}
/* 初始化服务器地址*/
addrlen=sizeof(struct sockaddr_in);
bzero(&ser_addr,addrlen);
ser_addr.sin_family=AF_INET;
ser_addr.sin_addr.s_addr=inet_addr(seraddr);
ser_addr.sin_port=htons(SERVER_PORT);
if(connect(cli_sockfd,(struct sockaddr*)&ser_addr,&addrlen)!=0)/*请求连接*/
{
/*连接失败 */
fprintf(stderr,"Connect Error:%s\n",strerror(errno));
close(cli_sockfd);
exit(1);
}
strcpy(msg,"hi,I am client!");
send(sockfd,msg,sizeof(msg),0);/*发送数据*/
recv(sockfd,msg,MAX_MSG_SIZE,0); /* 接受数据*/
printf("%s\n",msg);/*在屏幕上打印出来 */
close(cli_sockfd);
}
服务器的工作流程:首先调用socket函数创建一个Socket,然后调用bind函数将其与本机地址以及一个本地端口号绑定,接收到一个客户端时,服务器显示该客户端的IP地址,并将字串返回给客户端。
int main(int argc, char **argv)
{
int ser_sockfd;
int len;
// int addrlen;
socklen_t addrlen;
char seraddr[100];
struct sockaddr_in ser_addr;
/*建立socket*/
ser_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (ser_sockfd < 0)
{
printf("I cannot socket success\n");
return 1;
}
/*填写sockaddr_in 结构*/
addrlen = sizeof(struct sockaddr_in);
bzero(&ser_addr, addrlen);
ser_addr.sin_family = AF_INET;
ser_addr.sin_addr.s_addr = htonl(INADDR_ANY);
ser_addr.sin_port = htons(SERVER_PORT);
/*绑定服务器*/
if (bind(ser_sockfd, (struct sockaddr *)&ser_addr, addrlen) < 0)
{
printf("connect");
return 1;
}
while (1)
{
bzero(seraddr, sizeof(seraddr));
len = recvfrom(ser_sockfd, seraddr, sizeof(seraddr), 0, (struct sockaddr *)&ser_addr, &addrlen);
/*显示client端的网络地址*/
printf("receive from %s\n", inet_ntoa(ser_addr.sin_addr));
/*显示客户端发来的字串*/
printf("recevce:%s", seraddr);
/*将字串返回给client端*/
sendto(ser_sockfd, seraddr, len, 0, (struct sockaddr *)&ser_addr, addrlen);
}
}
客户端的工作流程:首先调用socket函数创建一个Socket,填写服务器地址及端口号,从标准输入设备中取得字符串,将字符串传送给服务器端,并接收服务器端返回的字符串。最后关闭该socket。
int GetServerAddr(char * addrname)
{
printf("please input server addr:");
scanf("%s",addrname);
return 1;
}
int main(int argc,char **argv)
{
int cli_sockfd;
int len;
socklen_t addrlen;
char seraddr[14];
struct sockaddr_in cli_addr;
char buffer[256];
GetServerAddr(seraddr);
/* 建立socket*/
cli_sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(cli_sockfd<0)
{
printf("I cannot socket success\n");
return 1;
}
/* 填写sockaddr_in*/
addrlen=sizeof(struct sockaddr_in);
bzero(&cli_addr,addrlen);
cli_addr.sin_family=AF_INET;
cli_addr.sin_addr.s_addr=inet_addr(seraddr);
//cli_addr.sin_addr.s_addr=htonl(INADDR_ANY);
cli_addr.sin_port=htons(SERVER_PORT);
bzero(buffer,sizeof(buffer));
/* 从标准输入设备取得字符串*/
len=read(STDIN_FILENO,buffer,sizeof(buffer));
/* 将字符串传送给server端*/
sendto(cli_sockfd,buffer,len,0,(struct sockaddr*)&cli_addr,addrlen);
/* 接收server端返回的字符串*/
len=recvfrom(cli_sockfd,buffer,sizeof(buffer),0,(structsockaddr*)&cli_addr,&addrlen);
//printf("receive from %s\n",inet_ntoa(cli_addr.sin_addr));
printf("receive: %s",buffer);
close(cli_sockfd);
}
CC=gcc
all:server client
CFLAGS=-o
server: server.c
$(CC) $(CFLAGS) $@ server.c
client: client.c
$(CC) $(CFLAGS) $@ client.c
clean:
rm -f server client
简单的回显服务器和客户端代码
udp_echo_server程序源码:mnxcc/linuxdc - Gitee.com
实现一个简单的英译汉的网络字典
udp_dict_server程序源码:mnxcc/linuxdc - Gitee.com
udp_chat_server程序源码:mnxcc/linuxdc - Gitee.com
UDP 协议支持全双工,一个 sockfd,既可以读取,又可以写入,对于客户端和服务端同样如此
基于 IPv4 的 socket 网络编程,sockaddr_in 中的成员 struct in_addr.sin_addr 表示 32 位 的 IP 地址in_addr转字符串的函数:
但是我们通常用点分十进制的字符串表示 IP 地址,以下函数可以在字符串表示 和 in_addr 表示之间转换;
其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void*addrptr。
inet_ntoa这个函数返回了一个char*,很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果 那么是否需要调用者手动释放呢?
man手册上说,inet_ntoa函数,是把这个返回结果放到了静态存储区.这个时候不需要我们手动进行释放
那么问题来了,如果我们调用多次这个函数,会有什么样的效果呢?
因为inet_ntoa把结果放到自己内部的一个静态存储区,这样第二次调用时的结果会覆盖掉上一次的结果
多线程调用inet_ntoa代码示例如下
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
void *Func1(void *p)
{
struct sockaddr_in *addr = (struct sockaddr_in *)p;
while (1)
{
char *ptr = inet_ntoa(addr->sin_addr);
printf("addr1: %s\n", ptr);
}
return NULL;
}
void *Func2(void *p)
{
struct sockaddr_in *addr = (struct sockaddr_in *)p;
while (1)
{
char *ptr = inet_ntoa(addr->sin_addr);
printf("addr2: %s\n", ptr);
}
return NULL;
}
int main()
{
pthread_t tid1 = 0;
struct sockaddr_in addr1;
struct sockaddr_in addr2;
addr1.sin_addr.s_addr = 0;
addr2.sin_addr.s_addr = 0xffffffff;
pthread_create(&tid1, NULL, Func1, &addr1);
pthread_t tid2 = 0;
pthread_create(&tid2, NULL, Func2, &addr2);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
tcp_echo_server程序源码:mnxcc/linuxdc - Gitee.com
command_server程序源码:mnxcc/linuxdc - Gitee.com
我们程序员写的一个个解决我们实际问题, 满足我们日常需求的网络程序, 都是在应用层
协议是一种 "约定". socket api 的接口, 在读写数据时, 都是按 "字符串" 的方式来发送接收的. 如果我们要传输一些 "结构化的数据" 怎么办呢?
其实,协议就是双方约定好的结构化的数据
所以:
cal_server程序源码:mnxcc/linuxdc - Gitee.com
例如, 我们需要实现一个服务器版的加法器. 我们需要客户端把要计算的两个加数发过去, 然后由服务器进行计算, 最后再把结果返回给客户端
约定方案一:
约定方案二:
无论我们采用方案一, 还是方案二, 还是其他的方案, 只要保证, 一端发送时构造的数据,在另一端能够正确的进行解析, 就是 ok 的. 这种约定, 就是 应用层协议
但是,为了让深刻理解协议,我们自定义实现一下协议的过程
期望的报文格式
所以,完整的处理过程应该是:
Jsoncpp 是一个用于处理 JSON 数据的 C++ 库。它提供了将 JSON 数据序列化为字符串以及从字符串反序列化为 C++ 数据结构的功能。Jsoncpp 是开源的,广泛用于各种需要处理 JSON 数据的 C++ 项目中
ubuntu:sudo apt-get install libjsoncpp-dev
Centos: sudo yum install jsoncpp-devel
序列化指的是将数据结构或对象转换为一种格式,以便在网络上传输或存储到文件 中。Jsoncpp 提供了多种方式进行序列化:
1.使用 Json::Value 的 toStyledString 方法:
优点:将 Json::Value 对象直接转换为格式
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
int main()
{
Json::Value root;
root["name"] = "joe";
root["sex"] = "男";
std::string s = root.toStyledString();
std::cout << s << std::endl;
return 0;
}
2.使用 Json::StreamWriter
优点:提供了更多的定制选项,如缩进、换行符等
#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>
int main()
{
Json::Value root;
root["name"] = "joe";
root["sex"] = "男";
Json::StreamWriterBuilder wbuilder; // StreamWriter 的工厂
std::unique_ptr<Json::StreamWriter>
writer(wbuilder.newStreamWriter());
std::stringstream ss;
writer->write(root, &ss);
std::cout << ss.str() << std::endl;
return 0;
}
3.使用 Json::FastWriter:
优点:比 StyledWriter 更快,因为它不添加额外的空格和换行符
#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>
int main()
{
Json::Value root;
root["name"] = "joe";
root["sex"] = "男";
Json::FastWriter writer;
std::string s = writer.write(root);
std::cout << s << std::endl;
return 0;
}
反序列化指的是将序列化后的数据重新转换为原来的数据结构或对象。Jsoncpp 提供了以下方法进行反序列化
1.使用 Json::Reader:
优点:提供详细的错误信息和位置,方便调试
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
int main()
{
// JSON 字符串
std::string json_string = "{\"name\":\"张三\",\"age\":30, \"city\":\"北京\"}";
// 解析 JSON 字符串
Json::Reader reader;
Json::Value root;
// 从字符串中读取 JSON 数据
bool parsingSuccessful = reader.parse(json_string,
root);
if (!parsingSuccessful)
{
// 解析失败,输出错误信息
std::cout << "Failed to parse JSON: " << reader.getFormattedErrorMessages() << std::endl;
return 1;
}
// 访问 JSON 数据
std::string name = root["name"].asString();
int age = root["age"].asInt();
std::string city = root["city"].asString();
// 输出结果
std::cout << "Name: " << name << std::endl;
std::cout << "Age: " << age << std::endl;
std::cout << "City: " << city << std::endl;
return 0;
}
2. 使用 Json::CharReader 的派生类:
在某些情况下,你可能需要更精细地控制解析过程,可以直接使用 Json::CharReader 的派生类
但通常情况下,使用 Json::parseFromStream 或 Json::Reader 的 parse 方法就足够了
Json::Value 是 Jsoncpp 库中的一个重要类,用于表示和操作 JSON 数据结构。以下是一些常用的 Json::Value 操作列表