Linux--网络编程(含windows)

一、网络通信socket

计算机是通过TCP/IP协议进行互联从而进行通信的,为了把复杂的TCP/IP协议隐藏起来,更方便的实现计算机中两个程序进行通信,引出了socket这个概念。
Alt

socket翻译为套接字,可以理解为IP地址与端口号的组合。
socket提供了流(stream)和数据报(datagram)两种通信机制,即流socket(SOCK_STREAM)和数据报socket(SOCK_DGRAM)。

流socket基于TCP协议,是一个有序、可靠、双向字节流的通道,传输数据不会丢失、不会重复、顺序也不会错乱。就像两个人在打电话,接通后就在线了,您一句我一句的聊天。

数据报socket基于UDP协议,不需要建立和维持连接,可能会丢失或错乱。UDP不是一个可靠的协议,对数据的长度有限制,但是它的速度比较高。就像短信功能,一个人向另一个人发短信,对方不一定能收到。

在实际开发中,数据报socket的应用场景极少,绝大部分情况下使用流套接字(SOCK_STREAM)。

二、网络通信的过程

在TCP/IP网络应用中,两个程序之间通信模式是客户/服务端模式(client/server),客户/服务端也叫作客户/服务器。服务端与客户端网络通信的交互过程如下图所示。
Alt
图中使用了socket(),connect等函数是网络通信常用的API。对于程序员来说,只要用好socket相关的函数,就可以完成网络通信。

三、网络编程常用API

A、客户端网络通信过程与常用API

根据客户端的工作流程一次介绍如下套接字基本函数:

  1. socket()
  2. connect()
  3. send()/recv()
  4. close()

1, 创建套接字:int socket(int family,int type,int protocol);

功能介绍:在Linux系统中,一切皆文件。为了表示和区分已经打开的文件,UNIX/Linux会给文件分配一个ID,这个ID就是一个整数,被称为文件描述符。因此,网络连接也是一个文件,它也有文件描述符。通过socket()函数来创建一个网络连接或者说打开一个网络文件,socket()函数的返回值就是文件描述符,通过这个文件描述符我们就可以使用普通的文件操作来传输数据了。
参数介绍:
参数一:family 代表协议族,在socket中只能是AF_INET。
参数二:type 代表协议类型常见类型是SOCK_STREAM(TCP),SOCK_DGRAM(UDP)
参数三:protocol 代表具体的协议,对于标准套接字来说,其值是0。(原始套接字基本不会使用)

2,客户端请求连接函数: int connect(int sock_fd, struct sockaddr *serv_addr,int addrlen);

功能介绍:客户端向服务端发起连接请求,当返回值是0时代表连接成功,返回值为-1时代表连接失败。
参数介绍:
参数一:sock_fd代表通过socket()函数返回的文件描述符
参数二:serv_addr 代表目标服务器的协议族,网络地址以及端口号。是一个sockaddr 类型的指针。
参数三:addrlen 代表第二个参数内容的大小。

3,客户端发送与接收数据函数:

3.1,int send(int sockfd,const void *buf,int len,int flags);

功能介绍:send函数用于把数据通过socket发送给对端。不论是客户端还是服务端,应用程序都用send函数来向TCP连接的另一端发送数据。客户端通过该函数向服务器应用程序发送数据。函数返回已发送的字节数。出错时返回-1
参数介绍:
参数一:sockfd代表 发送端的套接字描述符,即通过socket()函数返回的文件描述符。
参数二:buf 指明需要发送数据的内存地址,可以是C语言基本数据类型变量的地址,也可以是数组、结构体、字符串。
参数三: len 指明实际发送数据的字节数。
参数四:flags 一般设置为0,其他数值意义不大。

3.2,int recv(int sockfd,void *buf,int len,int flags);

功能介绍:recv函数用于接收对端socket发送过来的数据。不论是客户端还是服务端,应用程序都用recv函数接受来自TCP连接的另一端发送过来的数据。
如果socket对端没有发送数据,recv函数就会等待,如果对端发送了数据,函数返回接收到的字符数。出错时返回-1。如果socket被对端关闭,返回值为0。

参数介绍:
参数一:sockfd代表接收端的套接字描述符,即通过socket()函数返回的文件描述符。
参数二:buf 为用于接收数据的内存地址,可以是C语言基本数据类型变量的地址,也可以是数组、结构体、字符串。只要是一块内存就行了。
参数三:len 指明需要接收数据的字节数。不能超过buf的大小,否则内存溢出。
参数四:flags填0,其他数值意义不大。

4,int close(int sockfd);

函数功能:关闭套接字,并终止TCP连接。若成功则返回0.失败则返回-1;
函数参数:
参数一:sockfd代表接收端的套接字描述符,即通过socket()函数返回的文件描述符。

B、服务端网络通信过程与常用API

根据客户端的工作流程一次介绍如下套接字基本函数:

  1. socket()
  2. bind()
  3. listen()
  4. accept()
  5. recv()/send()
  6. close()
    一部分函数已经介绍过,接下来介绍另一部分函数。

C、服务端网络通信过程与常用API

1,套接字绑定函数:int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

函数功能:服务端把用于通信的地址和端口绑定到socket上,当bind函数返回0时,为正确绑定,返回-1,则为绑定失败。
参数介绍:
参数一:sockfd 代表需要绑定的socket。是在创建socket套接字时返回的文件描述符。
参数二:addr 存放了服务端用于通信的地址和端口。
参数三:addrlen 代表addr结构体的大小。

2,监听函数:int listen(int sockfd, int backlog);

函数功能:listen函数的功能并不是等待一个新的connect的到来,真正等待connect的是accept函数。isten的操作就是当有较多的client发起connect时,server端不能及时的处理已经建立的连接,这时就会将connect连接放在等待队列中缓存起来。这个等待队列的长度有listen中的backlog参数来设定。当listen运行成功时,返回0;运行失败时,返回值为-1.

参数说明:
参数一:sockfd是前面socket创建的文件描述符;
参数二:backlog是指server端可以缓存连接的最大个数,也就是等待队列的长度。

3,接收请求函数:int accept(int sockfd,struct sockaddr *client_addr,socklen_t *addrlen);

函数功能:accept函数等待客户端的连接,如果没有客户端连上来,它就一直等待,这种方式称为阻塞。accept等待到客户端的连接后,创建一个新的socket,函数返回值就是这个新的socket,服务端用于这个新的socket和客户端进行报文的收发。
参数介绍:
参数一:sockfd 是已经被listen过的socket。
参数二:client_addr 用于存放客户端的地址信息,其中包含客户端的协议族,网络地址以及端口号。如果不需要客户端的地址,可以填0。
参数三:addrlen 用于存放参数二(client_addr)的长度

四、C++网络编程小案例

以下是写的Linux系统的一个服务器与多个客户端通信的代码,其中有使用到多线程。没学过多线程的还需要先去学习一下C语言多线程技术。
其中服务器代码为server.c
下面展示服务器的全部代码 server.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <pthread.h>

#define nums 128

typedef struct 
{
    int fd;                      // 通信
    pthread_t tid;               // 线程ID
    struct sockaddr_in addr;     // 地址信息
}SockInfo;

SockInfo infos[nums];

void* working(void* arg) {
    SockInfo* info = (SockInfo*)arg;

    //打印客户端的地址信息
    char ip[32] = { 0 };  //网络ip转本地小端的函数inet_ntop,第三、四个参数需要
    printf("client ip is: %s, nport is: %d\n", inet_ntop(AF_INET, &info->addr.sin_addr.s_addr, ip, sizeof(ip)),
        ntohs(info->addr.sin_port));

    //5.和客户端通信

    while (1)
    {
        //接收数据
        char buffer[1024];

        //接收函数 recv 或 read都可以
        int len = recv(info->fd, buffer, sizeof(buffer), 0);
        if (len > 0) {
            printf("client say: %s\n", buffer);
            //回复客户端
            send(info->fd, buffer, len, 0);
        }
        else if (len == 0) {
            printf("client was close...\n");
            break;
        }
        else {
            perror("recv");
            break;
        }

    }
    close(info->fd);
    info->tid = -1;
    info->fd = -1;

    return NULL;
}

int main()
{
    // 1.创建监听的套接字
    int lfd = socket(AF_INET,SOCK_STREAM,0);

    if (lfd==-1) {
        perror("socket");
        exit(0);
    }
    
    //2.将监听套接字与本地ip、端口绑定在一起
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(30001);   //转成大端端口
    /*
       INADDR_ANY代表本机的所有IP, 假设有三个网卡就有三个IP地址
       这个宏可以代表任意一个IP地址
       这个宏一般用于本地的绑定操作
    */
    addr.sin_addr.s_addr = INADDR_ANY;   // 这个宏的值为0 == 0.0.0.0
    //绑定
    int ret = bind(lfd,(struct sockaddr*) &addr, sizeof(addr));
    if (ret==-1) {
        perror("bind");
        exit(0);
    }
    
    //3.设置监听
    int lit = listen(lfd,128);  //最多128个
    if (lit == -1) {
        perror("listen");
        exit(0);
    }

    //初始化infos
    for (int i = 0; i < nums; i++)
    {
        bzero(&infos[i], sizeof(infos[i]));  //结构体初始化为0
        infos[i].fd = -1;
        infos[i].tid = -1;
    }

    //主线程监听并创建子线程通信
    while (1)
    {
        SockInfo* ptr;
        //从infos结构体找一个未使用的
        for (int i = 0; i < nums; i++)
        {
            if (infos[i].fd == -1) {
                ptr = &infos[i];
                break;
            }
            //如果客户端占满了,睡眠1秒等待新的客户端空闲位置
            if (i == nums - 1)
            {
                sleep(1);
                i--;
            }
        }

        //4.阻塞等待并接收客户端连接
        int cliaddr_len = sizeof(struct sockaddr_in);
        int clienfd = accept(lfd, (struct sockaddr*)&ptr->addr, &cliaddr_len);   //accept函数返回通信套接字clienfd
        printf("parent thread, connfd: %d\n", clienfd);
        if (clienfd == -1) {
            perror("accept");
            exit(0);
        }
        //创建子线程
        ptr->fd = clienfd;
        pthread_create(&ptr->tid,NULL,working,ptr);
        //让子线程和主线程分离
        pthread_detach(ptr->tid);

    }

    
    //5.断开连接, 关闭套接字
    close(lfd);  // 监听套接字
   
    return 0;
}

客户端Linux系统的代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>

#define IP "服务器的ip地址"

int main()
{
    // 1.创建通信的套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);

    if (fd == -1) {
        perror("socket");
        exit(0);
    }

    //2.连接服务器
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(30001);   //转成大端端口
    inet_pton(AF_INET,IP,&addr.sin_addr.s_addr);
    //连接
    int ret = connect(fd,(struct sockaddr*) &addr,sizeof(addr));
    if (ret == -1) {
        perror("connect");
        exit(0);
    }
    
    
    //3.和服务器通信
    int number = 0; //通信次数
    while (1)
    {
        char buffer[1024];
        sprintf(buffer,"hello,server...%d\n",number++);
        //发数据到服务器
        send(fd,buffer,strlen(buffer)+1,0);


        //接收服务器传来的数据
        memset(buffer,0,sizeof(buffer));
        int len = recv(fd,buffer,sizeof(buffer),0);
        if (len > 0 ) {
            printf("server say: %s\n",buffer);
        }
        else if (len==0) {
            printf("server was close...\n");
            break;
        }
        else {
            perror("recv");
            break;
        }
        sleep(1);  //每隔1秒发送一条数据

    }

    //4.断开连接, 关闭套接字
    close(fd);  
   

    return 0;
}

客户端Windows系统的代码
【注意】:在windows系统上,#include <arpa/inet.h>没用,需要更改为#include <winsock2.h> ,并且需要连接动态库 -lws2_32。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <winsock2.h> 

#define IP "服务器的ip地址"

int main()
{
	WSADATA wsa; // 正确的类型名
    int result;

    // 初始化 Winsock
    result = WSAStartup(MAKEWORD(2,2), &wsa);
	
	
    // 1.创建通信的套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);

    if (fd == -1) {
        perror("socket");
        exit(0);
    }

    //2.连接服务器
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(30001);   //转成大端端口
    inet_pton(AF_INET,IP,&addr.sin_addr.s_addr);
    //连接
    int ret = connect(fd,(struct sockaddr*) &addr,sizeof(addr));
    if (ret == -1) {
        perror("connect");
        exit(0);
    }
    
    
    //3.和服务器通信
    int number = 0; //通信次数
    while (1)
    {
        char buffer[1024];
        sprintf(buffer,"hello,server...%d\n",number++);
        //发数据到服务器
        send(fd,buffer,strlen(buffer)+1,0);


        //接收服务器传来的数据
        memset(buffer,0,sizeof(buffer));
        int len = recv(fd,buffer,sizeof(buffer),0);
        if (len > 0 ) {
            printf("server say: %s\n",buffer);
        }
        else if (len==0) {
            printf("server was close...\n");
            break;
        }
        else {
            perror("recv");
            break;
        }
        sleep(1);  //每隔1秒发送一条数据

    }

    //4.断开连接, 关闭套接字
    close(fd);  
   	// 注销Winsock相关库
	WSACleanup();

    return 0;
}

以上就是一个简易的利用Socket+多线程LinunxWindows上实现的网络编程小案例,记录生活、记录美好,希望可以帮助到正在浏览的你。

一部分内容来源于CSDN博主「镜花寒」
原文链接:https://blog.csdn.net/ZH0314/article/details/77387162

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值