返回

Linux下的socket网络通信

想来虽然学了计网,但却没有真正的用程序来实现一下计网的理论,于是就学习了一下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、与客户端通信,接受客户端发送的信息,并返回响应

通过recvsend函数进行通信

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发送给服务器,每发送一个消息等待回复

同样使用recvsend函数

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实例讲解

[3] sockaddr和sockaddr_in详解

你要相信流星划过会带给我们幸运,就像现实告诉你我要心存感激
Built with Hugo
Theme Stack designed by Jimmy