C++ 网络编程] Socket 基础:网络通信程序入门级教程
????一、基本概念
网络通讯是指两台计算机中的程序进行传输消息的过程。
- 客户端∶指主动发起通讯的程序。 客户端必须提前知道服务端的IP地址和通讯端口。
- 服务端∶指被动的等待,然后为向它发起通讯的客户端提供服务。 服务端不需要知道客户端的IP地址。
????二、第一个网络通讯程序
????2.1 网络通讯的流程示意图
????2.2 程序模块
客户端client.cpp
/*
* 程序名:client.cpp,此程序用于演示socket的客户端
*/
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
using namespace std;
int main(int argc,char *argv[])
{
if (argc!=3)
{
cout << "Using:./client 服务端的IP 服务端的端口\nExample:./client 192.168.101.139 5005\n\n";
return -1;
}
/* 第1步:创建客户端的socket。 */
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if (sockfd==-1)
{
perror("socket"); return -1;
}
/* 第2步:向服务器发起连接请求。*/
struct hostent* h; // 用于存放服务端IP的结构体。
if ( (h = gethostbyname(argv[1])) == 0 ) // 把字符串格式的IP转换成结构体。
{
cout << "gethostbyname failed.\n" << endl; close(sockfd); return -1;
}
struct sockaddr_in servaddr; // 用于存放服务端IP和端口的结构体。
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET;
memcpy(&servaddr.sin_addr,h->h_addr,h->h_length); // 指定服务端的IP地址。
servaddr.sin_port = htons(atoi(argv[2])); // 指定服务端的通信端口。
if (connect(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr))!=0) // 向服务端发起连接清求。
{
perror("connect"); close(sockfd); return -1;
}
/* 第3步:与服务端通讯,客户发送一个请求报文后等待服务端的回复,收到回复后,再发下一个请求报文。*/
char buffer[1024];
for (int ii=0;ii<3;ii++) // 循环3次,将与服务端进行三次通讯。
{
int iret;
memset(buffer,0,sizeof(buffer));
sprintf(buffer,"这是第%d个超级女生,编号%03d。",ii+1,ii+1); // 生成请求报文内容。
// 向服务端发送请求报文。
if ( (iret=send(sockfd,buffer,strlen(buffer),0))<=0)
{
perror("send"); break;
}
cout << "发送:" << buffer << endl;
memset(buffer,0,sizeof(buffer));
/* 接收服务端的回应报文,如果服务端没有发送回应报文,recv()函数将阻塞等待。*/
if ( (iret=recv(sockfd,buffer,sizeof(buffer),0))<=0)
{
cout << "iret=" << iret << endl; break;
}
cout << "接收:" << buffer << endl;
sleep(1);
}
/* 第4步:关闭socket,释放资源。*/
close(sockfd);
}
服务端server.cpp
/*
* 程序名:server.cpp,此程序用于演示socket通信的服务端
*/
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
using namespace std;
int main(int argc,char *argv[])
{
if (argc!=2)
{
cout << "Using:./server 通讯端口\nExample:./server 5005\n\n"; // 端口大于1024,不与其它的重复。
cout << "注意:运行服务端程序的Linux系统的防火墙必须要开通5005端口。\n";
cout << " 如果是云服务器,还要开通云平台的访问策略。\n\n";
return -1;
}
// 第1步:创建服务端的socket。
int listenfd = socket(AF_INET,SOCK_STREAM,0);
if (listenfd==-1)
{
perror("socket"); return -1;
}
// 第2步:把服务端用于通信的IP和端口绑定到socket上。
struct sockaddr_in servaddr; // 用于存放服务端IP和端口的数据结构。
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET; // 指定协议。
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 服务端任意网卡的IP都可以用于通讯。
servaddr.sin_port = htons(atoi(argv[1])); // 指定通信端口,普通用户只能用1024以上的端口。
// 绑定服务端的IP和端口。
if (bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)) != 0 )
{
perror("bind"); close(listenfd); return -1;
}
// 第3步:把socket设置为可连接(监听)的状态。
if (listen(listenfd,5) != 0 )
{
perror("listen"); close(listenfd); return -1;
}
// 第4步:受理客户端的连接请求,如果没有客户端连上来,accept()函数将阻塞等待。
int clientfd=accept(listenfd,0,0);
if (clientfd==-1)
{
perror("accept"); close(listenfd); return -1;
}
cout << "客户端已连接。\n";
// 第5步:与客户端通信,接收客户端发过来的报文后,回复ok。
char buffer[1024];
while (true)
{
int iret;
memset(buffer,0,sizeof(buffer));
// 接收客户端的请求报文,如果客户端没有发送请求报文,recv()函数将阻塞等待。
// 如果客户端已断开连接,recv()函数将返回0。
if ( (iret=recv(clientfd,buffer,sizeof(buffer),0))<=0)
{
cout << "iret=" << iret << endl; break;
}
cout << "接收:" << buffer << endl;
strcpy(buffer,"ok"); // 生成回应报文内容。
// 向客户端发送回应报文。
if ( (iret=send(clientfd,buffer,strlen(buffer),0))<=0)
{
perror("send"); break;
}
cout << "发送:" << buffer << endl;
}
// 第6步:关闭socket,释放资源。
close(listenfd); // 关闭服务端用于监听的socket。
close(clientfd); // 关闭客户端连上来的socket。
}
????三、运行测试
小伙伴可以在虚拟机或者购买服务器上运行喔,要求ubuntu环境。可以连接到同一个服务器,也可以连接到多个不同的服务器,这里我们指定一个为客户端client,一个为服务端server。
这里我使用的是xshell和xftp连接云服务器。
????3.1 准备工作
将上面两个cpp文件创建完成,显示如下:
使用xshell两次连接到同一个服务器【有多个服务器也可以让xshell连接到不同的服务器】,这里我们指定一个为客户端client,一个为服务端server。
????3.2 编译
客户端:
//编译client.cpp命令
g++ -g -o client client.cpp
服务端:
//编译server.cpp命令
g++ -g -o server server.cpp
????3.3 运行
查看服务器的IP地址
ip addr show
IP地址显示是172.23.23.16
服务端
打开服务端的连接,让服务器处于监听状态(ctrl+c可以退出):
//2003是端口号
./server 2003
客户端
再打开客户端的连接,让其访问服务端+建立连接:
//172.23.23.16是IP地址,2003是端口号
./client 172.23.23.16 2003
查看服务端内容:
????四、程序分析
????4.1 客户端程序
client.cpp是一个简单的客户端程序,用于与服务器进行通讯。让我们分步来看:
命令行参数检查:
if (argc!=3)
{
cout << "Using:./client 服务端的IP 服务端的端口\nExample:./client 192.168.101.139 5005\n\n";
return -1;
}
这段代码是在程序开始时对命令行参数进行检查。程序预期接收两个参数:服务端的IP地址和端口号。argc
表示命令行参数的数量,argv
是一个指向参数数组的指针。
argc != 3
:检查参数数量是否等于3,如果不等于3,说明用户没有提供正确的参数数量。
这里执行客户端命令用的是./client 172.23.23.16 2003。参数分别是:
-
./client
:表示程序名称。 -
172.23.23.16
:表示服务端的IP地址。 -
2003
:表示服务端的通讯端口。
1.创建客户端socket:
/* 第1步:创建客户端的socket。 */
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if (sockfd==-1)
{
perror("socket"); return -1;
}
这段程序的作用是创建客户端的套接字(socket),并进行创建的错误检查。程序分析:
-
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
这行代码创建了一个套接字,其中:-
AF_INET
指定了套接字的地址族为IPv4。 -
SOCK_STREAM
指定了套接字的类型为流式套接字,即TCP套接字。 -
0
表示使用默认的协议。
-
-
if (sockfd == -1)
这个条件判断检查套接字是否创建成功。如果套接字创建失败,socket()
函数返回-1
,程序通过perror("socket")
输出相关错误信息,然后返回-1
表示程序执行失败。
2.将服务端发送连接请求:
/* 第2步:向服务器发起连接请求。*/
struct hostent* h; // 用于存放服务端IP的结构体。
if ( (h = gethostbyname(argv[1])) == 0 ) // 把字符串格式的IP转换成结构体。
{
cout << "gethostbyname failed.\n" << endl; close(sockfd); return -1;
}
struct sockaddr_in servaddr; // 用于存放服务端IP和端口的结构体。
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET;
memcpy(&servaddr.sin_addr,h->h_addr,h->h_length); // 指定服务端的IP地址。
servaddr.sin_port = htons(atoi(argv[2])); // 指定服务端的通信端口。
if (connect(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr))!=0) // 向服务端发起连接清求。
{
perror("connect"); close(sockfd); return -1;
}
这段程序的作用是向服务器发起连接请求。程序分析:
-
struct hostent* h;
声明了一个指向hostent
结构体的指针,用于存放服务端的IP地址信息。 -
if ((h = gethostbyname(argv[1])) == 0)
调用gethostbyname()
函数,把服务端IP地址从字符串类型转换成hostent
结构体。如果转换失败(返回值为0),则输出错误信息并关闭套接字sockfd
,然后返回 -1。 -
struct sockaddr_in servaddr;
声明sockaddr_in
结构体变量servaddr
,用于存放服务端的IP地址和端口号信息。 -
memset(&servaddr, 0, sizeof(servaddr));
使用memset()
函数将servaddr
结构体清零,以便后续使用。 -
servaddr.sin_family = AF_INET;
设置servaddr
结构体中的地址族为 IPv4。 -
memcpy(&servaddr.sin_addr, h->h_addr, h->h_length);
将通过gethostbyname()
函数获取的服务端IP地址复制到servaddr
结构体中的sin_addr
字段。 -
servaddr.sin_port = htons(atoi(argv[2]));
将命令行参数中的服务端通信端口号转换成整数并转换成网络字节序(大端序),然后存放在servaddr
结构体中的sin_port
字段。 -
if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) != 0)
使用connect()
函数向服务端发起连接请求。如果连接失败,则输出错误信息,关闭套接字sockfd
,然后返回 -1。
3.与服务端通讯:
/* 第3步:与服务端通讯,客户发送一个请求报文后等待服务端的回复,
收到回复后,再发下一个请求报文。*/
char buffer[1024];
for (int ii=0;ii<3;ii++) // 循环3次,将与服务端进行三次通讯。
{
int iret;
memset(buffer,0,sizeof(buffer));
sprintf(buffer,"这是第%d个超级女生,编号%03d。",ii+1,ii+1); // 生成请求报文内容。
// 向服务端发送请求报文。
if ( (iret=send(sockfd,buffer,strlen(buffer),0))<=0)
{
perror("send"); break;
}
cout << "发送:" << buffer << endl;
memset(buffer,0,sizeof(buffer));
/* 接收服务端的回应报文,如果服务端没有发送回应报文,recv()函数将阻塞等待。*/
if ( (iret=recv(sockfd,buffer,sizeof(buffer),0))<=0)
{
cout << "iret=" << iret << endl; break;
}
cout << "接收:" << buffer << endl;
sleep(1);
}
通过一个循环,发送三次请求报文给服务端,每次发送后等待服务端的回复。发送和接收都使用send()
和recv()
函数,如果发送或接收失败则跳出循环。
这段程序是用来与服务端进行通讯的,具体作用如下:
-
char buffer[1024];
声明一个长度为1024的字符数组buffer
,用来存储通讯的数据。 -
for (int ii = 0; ii < 3; ii++)
循环3次,每次循环代表与服务端进行一次通讯。 -
memset(buffer, 0, sizeof(buffer));
清空buffer
数组,以确保没有之前的残留数据。 -
sprintf(buffer, "这是第%d个超级女生,编号%03d。", ii + 1, ii + 1);
使用sprintf()
函数将字符串格式化写入buffer
数组,形成一个请求报文的内容。 -
if ((iret = send(sockfd, buffer, strlen(buffer), 0)) <= 0)
通过send()
函数将请求报文发送给服务端,如果发送失败则输出错误信息,并跳出循环。 -
cout << "发送:" << buffer << endl;
输出发送的请求报文内容。 -
memset(buffer, 0, sizeof(buffer));
清空buffer
数组,以便接收服务端的回应。 -
if ((iret = recv(sockfd, buffer, sizeof(buffer), 0)) <= 0)
通过recv()
函数接收服务端的回应报文,如果接收失败或接收到的字节数为0则跳出循环。 -
cout << "接收:" << buffer << endl;
输出接收到的服务端的回应报文内容。 -
sleep(1);
程序暂停1秒钟,然后进行下一次通讯。
4.关闭socket:
/* 第4步:关闭socket,释放资源。*/
close(sockfd);
close()
函数用于关闭客户端套接字,释放资源。
????4.2 服务端程序
server.cpp是一个简单的服务端程序,用于与服务器进行通讯。让我们分步来看:
命令行参数检查:
if (argc!=2)
{
cout << "Using:./server 通讯端口\nExample:./server 5005\n\n"; // 端口大于1024,不与其它的重复。
cout << "注意:运行服务端程序的Linux系统的防火墙必须要开通5005端口。\n";
cout << " 如果是云服务器,还要开通云平台的访问策略。\n\n";
return -1;
}
首先,程序检查命令行参数数量是否为2,如果不是,则输出使用说明并退出程序。
在运行命令 ./server 2003
时,命令行参数如下:
-
./server
:表示程序名称。 -
2003
:表示通讯端口,即服务端程序将在2003端口上进行通讯。
1.创建服务端的socket:
// 第1步:创建服务端的socket。
int listenfd = socket(AF_INET,SOCK_STREAM,0);
if (listenfd==-1)
{
perror("socket"); return -1;
}
这段代码的作用是创建服务端的套接字(socket),并进行创建的错误检查。
-
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
这行代码创建了一个套接字,其中:-
AF_INET
指定了套接字的地址族为IPv4。 -
SOCK_STREAM
指定了套接字的类型为流式套接字,即TCP套接字,它提供了可靠的、双向的、基于连接的字节流。 -
0
表示使用默认的协议。
-
-
if (listenfd == -1)
这个条件判断检查套接字是否创建成功。如果套接字创建失败,socket()
函数返回-1
,程序通过perror("socket")
输出相关错误信息,然后返回-1
表示程序执行失败。
2.绑定IP地址和端口:
// 第2步:把服务端用于通信的IP和端口绑定到socket上。
struct sockaddr_in servaddr; // 用于存放服务端IP和端口的数据结构。
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET; // 指定协议。
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 服务端任意网卡的IP都可以用于通讯。
servaddr.sin_port = htons(atoi(argv[1])); // 指定通信端口,普通用户只能用1024以上的端口。
// 绑定服务端的IP和端口。
if (bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)) != 0 )
{
perror("bind"); close(listenfd); return -1;
}
这段代码的作用是将服务端用于通信的IP地址和端口绑定到先前创建的套接字上。让我们逐步解析:
-
struct sockaddr_in servaddr;
声明一个sockaddr_in
结构体变量servaddr
,用于存放服务端IP地址和端口号的信息。 -
memset(&servaddr, 0, sizeof(servaddr));
使用memset()
函数将servaddr
结构体清零,以确保没有之前的残留数据。 -
servaddr.sin_family = AF_INET;
设置servaddr
结构体中的地址族为 IPv4。 -
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
将servaddr
结构体中的IP地址设置为INADDR_ANY
,表示服务端可以使用任意可用的网络接口。 -
servaddr.sin_port = htons(atoi(argv[1]));
将命令行参数中的通信端口号转换成整数并转换成网络字节序(大端序),然后存放在servaddr
结构体中的sin_port
字段。 -
if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) != 0 )
通过bind()
函数将服务端IP地址和端口号与套接字进行绑定。如果绑定失败,输出错误信息,关闭套接字listenfd
,然后返回 -1 表示程序执行失败。
3.设置监听状态:
// 第3步:把socket设置为可连接(监听)的状态。
if (listen(listenfd,5) != 0 )
{
perror("listen"); close(listenfd); return -1;
}
这段代码的作用是将套接字设置为可连接(监听)的状态,以便服务端可以接受客户端的连接请求。
-
listen(listenfd, 5)
调用listen()
函数将套接字listenfd
设置为监听状态,并指定了可以同时处理的连接请求的最大数量为5。这意味着在任何时刻,服务器最多可以处理5个等待连接的客户端请求。 -
if (listen(listenfd, 5) != 0 )
检查listen()
函数是否执行成功,如果不成功,输出错误信息,关闭套接字listenfd
,然后返回 -1 表示程序执行失败。
设置套接字为监听状态后,服务端将能够接受客户端的连接请求,并把这些请求放入队列中等待处理。
4.接受客户端连接请求:
// 第4步:受理客户端的连接请求,如果没有客户端连上来,accept()函数将阻塞等待。
int clientfd=accept(listenfd,0,0);
if (clientfd==-1)
{
perror("accept"); close(listenfd); return -1;
}
cout << "客户端已连接。\n";
使用 accept()
函数受理客户端的连接请求,如果没有客户端连接上来,该函数将阻塞等待。
这段代码的作用是接受客户端的连接请求。让我们逐步解释:
-
int clientfd = accept(listenfd, 0, 0);
调用accept()
函数接受客户端的连接请求。参数listenfd
是之前通过socket()
和bind()
函数创建并绑定的监听套接字。函数会在有客户端连接请求到达时返回一个新的套接字clientfd
,用于与该客户端进行通信。 -
if (clientfd == -1)
检查accept()
函数是否成功接受客户端连接。如果返回值为-1
,表示发生了错误,通常是由于连接请求被拒绝或者出现了其他错误。在这种情况下,程序会输出错误信息,关闭监听套接字listenfd
,然后返回 -1 表示程序执行失败。 -
cout << "客户端已连接。\n";
如果accept()
成功接受了客户端的连接请求,程序会输出一条消息表示客户端已经连接上了。
综上所述,这段代码的作用是接受客户端的连接请求,并在成功连接时输出一条提示消息。
6.与客户端通信:
// 第5步:与客户端通信,接收客户端发过来的报文后,回复ok。
char buffer[1024];
while (true)
{
int iret;
memset(buffer,0,sizeof(buffer));
// 接收客户端的请求报文,如果客户端没有发送请求报文,recv()函数将阻塞等待。
// 如果客户端已断开连接,recv()函数将返回0。
if ( (iret=recv(clientfd,buffer,sizeof(buffer),0))<=0)
{
cout << "iret=" << iret << endl; break;
}
cout << "接收:" << buffer << endl;
strcpy(buffer,"ok"); // 生成回应报文内容。
// 向客户端发送回应报文。
if ( (iret=send(clientfd,buffer,strlen(buffer),0))<=0)
{
perror("send"); break;
}
cout << "发送:" << buffer << endl;
}
在一个循环中,不断接收客户端发送的请求报文,并回复 "ok"。
- 使用
recv()
函数接收客户端的请求报文,如果接收失败或客户端断开连接,则跳出循环。 - 使用
send()
函数向客户端发送回应报文。
这段代码是服务端与客户端进行通信的部分:
-
char buffer[1024];
声明一个长度为1024的字符数组buffer
,用于存储通讯的数据。 -
while (true)
进入一个无限循环,持续与客户端进行通信,直到客户端断开连接或发生错误。 -
memset(buffer, 0, sizeof(buffer));
清空buffer
数组,以确保没有之前的残留数据。 -
if ((iret = recv(clientfd, buffer, sizeof(buffer), 0)) <= 0)
通过recv()
函数接收客户端发送的请求报文,如果接收失败或客户端断开连接,则跳出循环。iret
变量存储接收到的字节数。 -
cout << "接收:" << buffer << endl;
输出接收到的客户端请求报文内容。 -
strcpy(buffer, "ok");
将回应报文内容设置为 "ok",表示服务端已接收到客户端的请求。 -
if ((iret = send(clientfd, buffer, strlen(buffer), 0)) <= 0)
通过send()
函数向客户端发送回应报文,如果发送失败,则输出错误信息,并跳出循环。 -
cout << "发送:" << buffer << endl;
输出发送的回应报文内容。
总体来说,这段代码实现了一个服务端与客户端的简单通信过程。服务端不断接收客户端发送的请求报文,然后回复 "ok" 表示已收到。
7.关闭socket,释放资源:
// 第6步:关闭socket,释放资源。
close(listenfd); // 关闭服务端用于监听的socket。
close(clientfd); // 关闭客户端连上来的socket。
这段代码的作用是关闭套接字并释放相关资源
-
close(listenfd);
关闭服务端用于监听客户端连接请求的套接字listenfd
。一旦服务端不再需要监听新的连接请求,可以关闭这个套接字,以释放相关资源并告知操作系统不再维护该套接字的状态信息。 -
close(clientfd);
关闭客户端连接的套接字clientfd
。一旦服务端与客户端的通信结束,可以关闭这个套接字,释放相关资源,并结束与该客户端的通信。
通过关闭套接字,程序能够清理掉所占用的系统资源,并确保程序的正常结束。
推荐阅读
-
C++ 网络编程] Socket 基础:网络通信程序入门级教程
-
go语言Socket编程-Socket编程 什么是Socket Socket,英文含义是插座、插孔,一般称之为套接字,用于描述IP地址和端口。可以实现不同程序间的数据通信。 Socket起源于Unix,而Unix基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。Socket就是该模式的一个实现,网络的Socket数据传输是一种特殊的I/O,Socket也是一种文件描述符。Socket也具有一个类似于打开文件的函数调用:Socket,该函数返回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现的。 套接字的内核实现较为复杂,不宜在学习初期深入学习,了解到如下结构足矣。 套接字通讯原理示意 在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识网络通讯中的一个进程。“IP地址+端口号”就对应一个socket。欲建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接。因此可以用Socket来描述网络连接的一对一关系。 常用的Socket类型有两种:流式Socket(SOCK_STREAM)和数据报式Socket(SOCK_DGRAM)。流式是一种面向连接的Socket,针对于面向连接的TCP服务应用;数据报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用。 网络应用程序设计模式 C/S模式 传统的网络应用设计模式,客户机(client)/服务器(server)模式。需要在通讯两端各自部署客户机和服务器来完成数据通信。 B/S模式 浏览器(Browser)/服务器(Server)模式。只需在一端部署服务器,而另外一端使用每台PC都默认配置的浏览器即可完成数据的传输。 优缺点 对于C/S模式来说,其优点明显。客户端位于目标主机上可以保证性能,将数据缓存至客户端本地,从而提高数据传输效率。且,一般来说客户端和服务器程序由一个开发团队创作,所以他们之间所采用的协议相对灵活。可以在标准协议的基础上根据需求裁剪及定制。例如,腾讯所采用的通信协议,即为ftp协议的修改剪裁版。 因此,传统的网络应用程序及较大型的网络应用程序都首选C/S模式进行开发。如,知名的网络游戏魔兽世界。3D画面,数据量庞大,使用C/S模式可以提前在本地进行大量数据的缓存处理,从而提高观感。 C/S模式的缺点也较突出。由于客户端和服务器都需要有一个开发团队来完成开发。工作量将成倍提升,开发周期较长。另外,从用户角度出发,需要将客户端安插至用户主机上,对用户主机的安全性构成威胁。这也是很多用户不愿使用C/S模式应用程序的重要原因。 B/S模式相比C/S模式而言,由于它没有独立的客户端,使用标准浏览器作为客户端,其工作开发量较小。只需开发服务器端即可。另外由于其采用浏览器显示数据,因此移植性非常好,不受平台限制。如早期的偷菜游戏,在各个平台上都可以完美运行。 B/S模式的缺点也较明显。由于使用第三方浏览器,因此网络应用支持受限。另外,没有客户端放到对方主机上,缓存数据不尽如人意,从而传输数据量受到限制。应用的观感大打折扣。第三,必须与浏览器一样,采用标准http协议进行通信,协议选择不灵活。 因此在开发过程中,模式的选择由上述各自的特点决定。根据实际需求选择应用程序设计模式。 简单的C/S模型通信 Server端:Listen函数 func Listen(network, address string) (Listener, error) network:选用的协议:TCP、UDP, 如:“tcp”或 “udp” address:IP地址+端口号, 如:“127.0.0.1:8000”或 “:8000” Listener 接口: type Listener interface { Accept (Conn, error) Close error Addr Addr } Conn 接口: type Conn interface { Read(b byte) (n int, err error) Write(b byte) (n int, err error) Close error LocalAddr Addr RemoteAddr Addr SetDeadline(t time.Time) error SetReadDeadline(t time.Time) error SetWriteDeadline(t time.Time) error } 参看 [<u>https://studygolang.com/pkgdoc</u>](https://studygolang.com/pkgdoc) 中文帮助文档中的demo: 示例代码:TCP服务器.go package main import ( "net" "fmt" ) func main { // 创建监听 listener, err:= net.Listen("tcp", ":8000") if err != nil { fmt.Println("listen err:", err) return } defer listener.Close // 主协程结束时,关闭listener fmt.Println("服务器等待客户端建立连接...") // 等待客户端连接请求 conn, err := listener.Accept if err != nil { fmt.Println("accept err:", err) return } defer conn.Close // 使用结束,断开与客户端链接 fmt.Println("客户端与服务器连接建立成功...") // 接收客户端数据 buf := make(byte, 1024) // 创建1024大小的缓冲区,用于read n, err := conn.Read(buf) if err != nil { fmt.Println("read err:", err) return } fmt.Println("服务器读到:", string(buf[:n])) // 读多少,打印多少。 }
-
Sugar Fly 教您 C++ Socket 网络编程 - 8.TCP 通信程序的异步通信版本
-
在Windows环境中实现C++和MFC编程的IPv6网络通信教程