想来虽然学了计网,但却没有真正的用程序来实现一下计网的理论,于是就学习了一下socket的通信,并进行复现。至于为什么是Linux,是发现网上好像没有太多系统的关于Windows的网络编程,索性就直接放弃了。
socket
socket即为套接字,是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。提供了应用进程利用网络协议交换数据的机制,上联应用程序,下联网络协议栈。既是应用协议通过网络协议进行通信的接口,也是应用程序与网络协议栈进行交互的接口。
因此socket将复杂的网络协议族(也就是TCP/IP),隐藏起来,使其对应用程序透明,利用socket可以实现两个程序之间的通信。
对于网络层的两大协议TCP和UDP,socket也分为了流和数据报的两种通信形式
- 流:基于TCP协议,因此有序,可靠。可以实现可靠传输。
- 数据报:基于UDP,不需要建立和维持连接,不可靠,但是通信速度较快。
C/S模式
在计算机网络中,两个程序的通信模式是,客户/服务器模式,双方借助socket建立连接后便能实现通信。
服务器工作流程
1、创建服务器socket
在这个阶段我们可以使用socket
函数来创建一个服务器的套接字。需要注意的是在socket编程中只能指定协议族为AF_INET
,即表示利用IPv4进行通信。这个函数的返回值为 文件描述符 (类型为整型),当失败时则会返回-1
如下
int listenfd;
listenfd = socket(AF_INET, SOCK_STREAM, 0)
2、将服务器用于通信的地址和端口绑定到socket上
在Linux中,地址信息被封装在了结构体sockaddr
中,如下
struct sockaddr {
sa_family_t sin_family; //地址族
char sa_data[14]; //14字节,包含套接字中的目标地址和端口信息
};
想必可以很明显的看到,在这个结构体中目标地址和端口信息是存放在一个数组中的,因此不便于区分,所以我们使用另外一个结构体sockaddr_in
,这个结构体如下
struct sockaddr_in {
sa_family_t sin_family;
uint16_t sin_port; // 端口号
struct in_addr sin_addr; // IP地址
char sin_zero;
}
但如果你进入到Linux的底层代码会发现,sockaddr_in
还有一个参数为sockaddr
的构造函数。因此使用这个结构体可以让操作更简单,如下
struct sockaddr_in servaddr;
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])); // 将主机字节顺序转换为网络字节顺序
之后将端口绑定。
bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr);
3、将socket设置为监听模式
listen(listenfd, 5); // 5表示准备接受5个连接
4、等待并接受客户端连接
使用accept
函数来连接客户端
int clientfd;
clientfd = accept(listenfd, (struct sockaddr*)&clientaddr, (socklen_t*)&socklen);
这里需要解释的是,服务器端其实建立了两个socket,第一个socket为被动的socket用来监听,当经过监听阶段后,Linux内核拥有accept函数借助监听的socket创建出连接客户端的socket
5、与客户端通信,接受客户端发送的信息,并返回响应
通过recv
和send
函数进行通信
while (true) {
int iret;
memset(buffer, 0, sizeof(buffer));
if ((iret = recv(clientfd, buffer, sizeof(buffer), 0)) <= 0) {
cout << "iret = " << iret << endl;
break;
}
cout << "receive " << buffer << endl;
strcpy(buffer, "ACCEPT");
if ((iret = send(clientfd, buffer, strlen(buffer), 0)) <= 0) {
perror("send");
break;
}
cout << "SEND: " << buffer << endl;
}
6、重复(5)中操作,直到客户端断开连接
7、释放socket连接
close(listenfd);
close(clientfd); // 释放监听socket和连接socket
代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<unistd.h>
#include<cstdlib>
#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 port" << endl;
cout << "Example: ./server 5005" << endl;
return -1;
}
// 1
int listenfd;
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket");
return -1;
}
// 2
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
//servaddr.sin_addr.s_addr = inet_addr("192.168.190.134"); // 手动指定IP地址
servaddr.sin_port = htons(atoi(argv[1]));
// 3
if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) != 0) {
perror("bind");
close(listenfd);
return -1;
}
if (listen(listenfd, 5) != 0) {
perror("listen");
close(listenfd);
return -1;
}
// 4
int clientfd;
int socklen = sizeof(struct sockaddr_in);
struct sockaddr_in clientaddr;
clientfd = accept(listenfd, (struct sockaddr*)&clientaddr, (socklen_t*)&socklen);
cout << "clinet has connected" << endl;
cout << "IP is: " << inet_ntoa(clientaddr.sin_addr) << endl;
// 5
char buffer[1024];
while (true) {
int iret;
memset(buffer, 0, sizeof(buffer));
//
if ((iret = recv(clientfd, buffer, sizeof(buffer), 0)) <= 0) {
cout << "iret = " << iret << endl;
break;
}
cout << "receive " << buffer << endl;
strcpy(buffer, "ACCEPT");
if ((iret = send(clientfd, buffer, strlen(buffer), 0)) <= 0) {
perror("send");
break;
}
cout << "SEND: " << buffer << endl;
}
close(listenfd);
close(clientfd);
}
客户端工作流程
1、创建客户端socket
同样采用socket
函数创建套接字,用来主动连接,与服务器的创建方式一样。
2、向服务器发起连接请求
使用gethostbyname
函数将IP地址转换为hostent
结构体所表示的格式。hostent
结构体如下
struct hostent {
char *h_name; //正式主机名
char **h_aliases; //主机别名
int h_addrtype; //主机IP地址类型:IPV4-AF_INET
int h_length; //主机IP地址字节长度,对于IPv4是四字节,即32位
char **h_addr_list; //主机的IP地址列表
};
建立连接的过程如下
int sockfd;
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET; // 手动指定相关参数
servaddr.sin_port = htons(atoi(argv[2]));
memcpy(&servaddr.sin_addr, hostcon->h_addr, hostcon->h_length);
connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr) // 连接
3、与服务器通信,将消息通过socket发送给服务器,每发送一个消息等待回复
同样使用recv
和send
函数
char buffer[1024];
for (int i = 0; i < 3; ++i) {
int iret;
memset(buffer, 0, sizeof(buffer));
sprintf(buffer, "this is %d data", i + 1);
if ((iret = send(sockfd, buffer, strlen(buffer), 0)) <= 0) {
perror("send");
break;
}
cout << "send: " << buffer << endl;
memset(buffer, 0, sizeof(buffer));
if ((iret = recv(sockfd, buffer, sizeof(buffer), 0)) <= 0) {
cout << "iret = " << iret << endl;
break;
}
cout << "receive " << buffer << endl;
}
4、重复操作(3),直至数据发送完毕
5、释放socket
close(sockfd);
代码
#include<cstdio>
#include<cstring>
#include<unistd.h>
#include<netdb.h>
#include<cstdlib>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<iostream>
using namespace std;
int main(int argc, char *argv[]) {
if (argc != 3) {
cout << "Using client port" << endl;
cout << "Example: ./client 127.0.0.1 5005" << endl;
return -1;
}
// 1
int sockfd;
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket");
return -1;
}
// 2
struct hostent* hostcon;
if ((hostcon = gethostbyname(argv[1])) == 0) {
cout << "gethostbyname failed" << endl;
close(sockfd);
return -1;
}
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(atoi(argv[2]));
memcpy(&servaddr.sin_addr, hostcon->h_addr, hostcon->h_length);
// 3
if (connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) != 0) {
perror("connect");
close(sockfd);
return -1;
}
// 4
char buffer[1024];
for (int i = 0; i < 3; ++i) {
int iret;
memset(buffer, 0, sizeof(buffer));
sprintf(buffer, "this is %d data", i + 1);
if ((iret = send(sockfd, buffer, strlen(buffer), 0)) <= 0) {
perror("send");
break;
}
cout << "send: " << buffer << endl;
memset(buffer, 0, sizeof(buffer));
if ((iret = recv(sockfd, buffer, sizeof(buffer), 0)) <= 0) {
cout << "iret = " << iret << endl;
break;
}
cout << "receive " << buffer << endl;
}
// 5
close(sockfd);
}
运行结果
在工程目录中打开两个终端,在编译后通过命令./server 5005
启动服务器,之后在另一个终端通过命令./client 127.0.0.1 5005
运行客户端,最终结果呈现如下。
参考
[1] 网络通信基础socket
[2] hostent实例讲解