Linux-socket编程

本文详细介绍了UDP和TCP套接字编程的基本流程、接口和代码实现。对于UDP,强调了其无连接、不可靠和面向数据报的特性,并展示了服务端和客户端的代码实现。对于TCP,讲解了面向连接、可靠性和面向字节流的特性,以及服务端监听、获取新连接和客户端发起连接的步骤,同时分析了多进程和多线程在处理并发连接时的区别和解决方案。
摘要由CSDN通过智能技术生成

目录

UDP-socket编程

UDP特性

编程流程

接口

代码实现

 TCP-socket编程

TCP特性

编程流程

接口

 原理

代码实现


UDP-socket编程

UDP特性

无连接:UDP双方在发送数据之前是不需要进行沟通的。只需要知道对方的ip地址和端口号就好。

不可靠:不保证UDP数据是可靠,有序的到达对端

面向数据报:UDP应用层/网络层递交数据的时候,都是整条数据进行交付的。

编程流程

服务端:创建套接字,绑定地址信息,接收数据,发送数据,关闭套接字。

客户端:创建套接字,绑定地址信息(不推荐),发送数据,接收数据,关闭套接字。

创建套接字绑定地址信息的含义:将进程与网卡绑定,进程可以从网卡中接收数据,也可以是通过网卡发送数据。绑定ip,绑定端口,是为了在网络上标识一台主机和一个进程。对于发送方而言,就知道收方在哪台机器的哪个进程了。对于接收方而言,就知道网络数据从哪机器的那个进程发出了。

接口

1.创建套接字接口

2.绑定接口

3. 发送接口

 4.接收接口

5.关闭套接字接口

代码实现

在实现代码之前,可以先使用命令ifconfig命令查看当前电脑的ip地址,方便后续绑定,如下:

 服务端代码如下:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(){
    //创建套接字
    int fd=socket(AF_INET,SOCK_DGRAM,0);
    if(fd<0){
        perror("socket");
        return 0;
    }
    struct sockaddr_in  addr;//地址信息的结构体
    addr.sin_family=AF_INET;//使用ipv4版本协议
    addr.sin_port=htons(9999);//转换为网络字节序
    addr.sin_addr.s_addr=inet_addr("10.0.24.6");//将ip地址转换为数字并且转换为网络字节序
    int ret=bind(fd,(struct sockaddr*)&addr,sizeof(addr));//绑定地址信息
    if(ret<0){
        perror("bind");
        return 0;
    }
    while(1){
        char buf[1024]={0};//将接收到的消息保存在buf中
        struct sockaddr_in src_addr;//用来保存recv_size接收到的客户端的地址信息
        socklen_t addrlen=sizeof(src_addr);
        ssize_t recv_size=recvfrom(fd,buf,sizeof(buf)-1,0,(struct sockaddr*)&src_addr,&addrlen);//接收消息,以及客户端地址信息
        if(recv_size<0){
            perror("recvfrom");
            return 0;
        }
        printf("%s,%d:%s\n",inet_ntoa(src_addr.sin_addr),ntohs(src_addr.sin_port),buf);//将接收到的消息以及源ip,源port,打印到屏幕
        memset(buf,0,sizeof(buf));//清空buf
        sprintf(buf,"i am serve");//往buf中写入内容
        sendto(fd,buf,strlen(buf),0,(struct sockaddr*)&src_addr,sizeof(src_addr));//给客户端回消息
    }
    close(fd);//关闭套接字描述符
    return 0;
}

编译运行:

 服务端阻塞在了recvfrom接口,现在正在等待客户端发送消息,现在可以使用命令netstat -anp | grep [端口号]来查看地址信息是否绑定成功,如下:

如果查询出现以上信息,说明地址信息绑定成功,注意:端口号是一个2字节16位的整数,只要不使用知名端口,就可以在此范围内随便给。

下面给出客户端的代码:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;
int main(int argc,char* argv[]){
    if(argc!=3){//命令行参数的个数必须是3个
        printf("./clint [ip] [port]\n");
        return 0;
    }
    char* ip=argv[1];//取出从命令行输入的ip
    int port=atoi(argv[2]);//将从命令行输入的字符串转化成整数
    int fd=socket(AF_INET,SOCK_DGRAM,0);//创建套接字
    if(fd<0){
        perror("socket");
        return 0;
    }
    struct sockaddr_in desaddr;//用来存放服务端的地址信息
    desaddr.sin_family=AF_INET;//使用ipv4协议
    desaddr.sin_port=htons(port);//将服务端的端口号转化为网络字节序并保存起来
    desaddr.sin_addr.s_addr=inet_addr(ip);//将服务端ip转化为数字,并转化为网络字节序保存起来
    while(1){
        printf("Please enter the content to send:");//提示用户输入要发送的内容
        fflush(stdout);//刷新缓冲区
        char buf[1024]={0};
        cin.getline(buf,1024);//接收用户发送的信息
        ssize_t send_size=sendto(fd,buf,strlen(buf),0,(struct sockaddr*)&desaddr,sizeof(desaddr));//把信息发送到服务端
        if(send_size<0){
            perror("sendto");
            return 0;
        }
        struct sockaddr_in souaddr;//用来保存服务端一会要发送的信息,这个可以不用写,因为服务端的地址信息已经知道了
        socklen_t addrlen=sizeof(souaddr);
        memset(buf,0,sizeof(buf));//清空buf
        ssize_t recv_size=recvfrom(fd,buf,sizeof(buf)-1,0,(struct sockaddr*)&souaddr,&addrlen);//接收服务端回的信息
        if(recv_size<0){
            perror("recvfrom");
            return 0;
        }
        printf("%s,%d:%s\n",inet_ntoa(souaddr.sin_addr),ntohs(souaddr.sin_port),buf);//将服务端回的信息打印出来
    }
    close(fd);
    return 0;
}

为什么客户端没有绑定地址信息呢?

其实在sendto函数内部,会检测当前进程有没有绑定地址信息,如果没有,操作系统会帮我们绑定地址信息。那么为什么我们不自己调用bind函数绑定?因为可能存在多个客户端,也就是说存在多个客户端进程。而代码只有一份,如果在代码中手动绑定了地址信息,意味着当前代码编译生成的可执行程序已经确定了要绑定的端口号,一旦启动后,端口号就被当前进程占用了,而在一台主机上端口号是不能重复的,再想启动相同的客户端就会失败。如果交给操作系统来给客户端分配端口号的话,是一定不会分配已经使用的端口号的,就可以运行多个客户端进程了。

编译运行后:

提示输入要发送的信息,下面来检测服务端是否可以收到客户端的信息:

客户端发送信息后,服务端可以正常接收到。

 TCP-socket编程

TCP特性

面向连接:TCP双方在发送数据之前需要建立好连接

可靠:TDP保证传输的数据是可靠,有序的到达对端

面向字节流:对于传输的数据没有明显的边界,接收方可以按照任意字节进行接收

编程流程

服务端:创建套接字,绑定地址信息,监听,获取新连接,接收数据,发送数据,关闭套接字

客户端:创建套接字,绑定地址信息(不推荐),发起连接,发送数据,接收数据,关闭套接字

接口

创建套接字、绑定地址信息、关闭套接字的接口与UDP相同,给出其他接口:

1.监听接口

2.获取新连接接口

3.发起连接接口

 4.发送接口

 5.接收接口

 原理

服务端调用listen接口监听后,等待客户端调用connect接口发起连接,连接建立后,服务端通过accept接口来获取新连接套接字,然后用这个新连接套接字来和客户端进行通信。

在解释的直白点:

服务端使用socket创建出来的套接字描述符,相当于“拉皮条的”,就是在侦听是否有新连接到来

服务端使用accept获取到的新连接套接字描述符(建立连接的过程在内核就已经完成了),相当于接客的“小红”,就是在同客户通信

代码实现

先来看客户端代码,跟UDP客户端不同的一点是发送数据时需要将连接先建立好,其他基本相同。

代码如下:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(){
    int fd=socket(AF_INET,SOCK_STREAM,0);///创建套接字
    if(fd<0){
        perror("socket");
        return 0;
    }
    sockaddr_in addr;//用来保存服务端的地址信息,也就是数据发往哪里
    addr.sin_family=AF_INET;//使用ipv4协议
    addr.sin_addr.s_addr=inet_addr("120.53.243.185");//将ip转化成数字,并转化为网络字节序
    addr.sin_port=htons(9999);//将port转化为网络字节序
    int ret=connect(fd,(sockaddr*)&addr,sizeof(addr));//发起连接,注意是在服务端监听后
    if(ret<0){
        perror("connect");
        return 0;
    }
    while(1){
        char buf[1024]={0};
        printf("Please enter your message:");//提示用户输入
        fflush(stdout);
        scanf("%s",buf);
        ret=send(fd,buf,strlen(buf),0);//给服务端发送数据
        if(ret<0){
            continue;
        }
        memset(buf,0,sizeof(buf));//清空buf
        ssize_t recvsize=recv(fd,buf,sizeof(buf)-1,0);//接收服务端发回的数据
        if(recvsize<0){
            perror("recv");
        }else if(recvsize==0){//recvsize==0,说明对端关闭了连接
            printf("The perr closed the connection\n");
            exit(1);
        }
        printf("%s\n",buf);

        }
    return 0;
}

服务端代码:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(){
    int fd=socket(AF_INET,SOCK_STREAM,0);//创建监听套接字
    if(fd<0){
        perror("socket");
        return 0;
    }
    struct sockaddr_in addr;
    addr.sin_family=AF_INET;//使用ipv4协议
    addr.sin_port=htons(9999);//将port转化为网络字节序并保存
    addr.sin_addr.s_addr=inet_addr("10.0.24.6");//将ip转化为数字并转化为网络字节序后保存
    int ret=bind(fd,(struct sockaddr*)&addr,sizeof(addr));//绑定地址信息
    if(ret<0){
        perror("bind");
        return 0;
    }
    listen(fd,5);//监听
    sockaddr_in cli_addr;//用来保存客户端的地址信息
    socklen_t cli_len=sizeof(cli_addr);//地址信息长度
    while(1){
        int newfd=accept(fd,(sockaddr*)&addr,&cli_len);//获取新连接套接字描述符
        if(newfd<0){
            perror("accept");
            return 0;
        }
        char buf[1024]={0};
        ssize_t recvsize=recv(newfd,buf,sizeof(buf)-1,0);
        if(recvsize<0){
            perror("recv");
            return 0;
        }else if(recvsize==0){//recvsize==0,说明对端关闭了连接
            printf("The peer close the connection\n");
        }
        printf("%s\n",buf);//打印服务端发送的信息
        memset(buf,0,sizeof(buf));//清空buf
        sprintf(buf,"%s,%d:i am serve,i get your message",inet_ntoa(addr.sin_addr),ntohs(addr.sin_port));
        send(newfd,buf,strlen(buf),0);//向服务端发送数据,注意要使用accept获取到的套接字描述符
    }
    return 0;
}

将客户端与服务端代码写完后,来测试是否能正常进行通信:

先启动服务端进行监听,然后再启动客户端1向服务端发送数据,接着启动客户端2向服务端发送数据。做完这些操作后,再使用客户端1向服务端发送数据,再使用客户端2向服务端发送数据,后面这两次发送都会失败,如下图:

这种现象说明服务端的代码是有问题的。存在两个问题:

1.在循环中accept如果获取不到新连接套接字就会阻塞掉,导致不能接收客户端发送的数据

2.每次获取到的新连接套接字描述符会被下一次获取到的新连接套接字描述符覆盖掉

那么如何解决以上问题呢?

可以采用两种解决方式:

1.多进程:服务端代码在获取到新连接套接字描述符后就创建子进程,让子进程拿着这个新连接套接字描述符去和客户端通信。如下图所示,子进程会将父进程files_struct也拷贝一份,那么在子进程中就也存在监听套接字描述符和新连接套接字描述符。

 注意:必须在父进程获取新连接套接字描述符之后再创建子进程,这样子进程才会拥有这个新连接套接字描述符。在创建完子进程后,为了防止资源浪费,父进程需要关闭这个新连接套接字描述符,而子进程需要关闭监听套接字描述符。

服务端代码如下:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
void SignalCallBack(int sig){
    printf("%d\n",sig);
    wait(NULL);
}
int main(){
    signal(SIGCHLD,SignalCallBack);
    int fd=socket(AF_INET,SOCK_STREAM,0);
    if(fd<0){
        perror("socket");
        return 0;
    }
    struct sockaddr_in addr;
    addr.sin_family=AF_INET;
    addr.sin_port=htons(9999);
    addr.sin_addr.s_addr=inet_addr("0.0.0.0");
    int ret=bind(fd,(struct sockaddr*)&addr,sizeof(addr));
    if(ret<0){
        perror("bind");
        return 0;
    }
    listen(fd,5);
    sockaddr_in cli_addr;
    socklen_t cli_len=sizeof(cli_addr);
    while(1){
        int newfd=accept(fd,(sockaddr*)&cli_addr,&cli_len);
        if(newfd<0){
            perror("accept");
            return 0;
        }
        pid_t pid=fork();//创建子进程
        if(pid<0){
            close(newfd);
        }else if(pid==0){//子进程逻辑:子进程拿着新连接套接字描述符去和客户端通信
            close(fd);//关闭监听套接字描述符
            while(1){
                char buf[1024]={0};
                ssize_t recvsize=recv(newfd,buf,sizeof(buf)-1,0);
                if(recvsize<0){
                    perror("recv");
                    exit(0);
                }else if(recvsize==0){
                    printf("close the connection:%s,%d\n",inet_ntoa(cli_addr.sin_addr),ntohs(cli_addr.sin_port));
                    exit(0);
                }
                printf("%s,%d:%s\n",inet_ntoa(cli_addr.sin_addr),ntohs(cli_addr.sin_port),buf);
                memset(buf,0,sizeof(buf));
                sprintf(buf,"%s,%d:i am serve,i get your message",inet_ntoa(cli_addr.sin_addr),ntohs(cli_addr.sin_port));
                send(newfd,buf,strlen(buf),0);
            }
        }else{//父进程逻辑
            close(newfd);//关闭新连接套接字描述符
        }
    }
    return 0;
}

编译运行,测试如下:

 使用命令查看父子进程此时的状态:

 

2.多线程:服务端代码在获取到新连接套接字描述符后就创建工作线程,让工作线程拿着这个新连接套接字描述符去和客户端通信。因为线程与线程之间共享文件描述符,所以使用多线程也可以达到目的。

代码如下:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
class Cli_info{//将新连接套接字描述符与客户端地址信息结构封装起来,以便于交给工作线程使用
public:
    int newfd;
    sockaddr_in cli_addr;
};
void* start(void* arg){//工作线程执行的代码,完成收发数据
    pthread_detach(pthread_self());//线程分离,线程退出后,可以不用手动回收
    Cli_info* cli=(Cli_info*)arg;
    int newfd=cli->newfd;//取出新连接套接字描述符
    sockaddr_in cli_addr=cli->cli_addr;//取出客户端地址信息
    while(1){//收发数据
        char buf[1024]={0};
        ssize_t recvsize=recv(newfd,buf,sizeof(buf)-1,0);
        if(recvsize<0){
            perror("recv");
            return 0;
        }else if(recvsize==0){
            printf("The peer close the connection:%s,%d\n",inet_ntoa(cli_addr.sin_addr),ntohs(cli_addr.sin_port));
            close(newfd);
            delete cli;
            pthread_exit(NULL);
        }
        printf("%s,%d:%s\n",inet_ntoa(cli_addr.sin_addr),ntohs(cli_addr.sin_port),buf);
        memset(buf,0,sizeof(buf));
        sprintf(buf,"%s,%d:i am serve,i get your message",inet_ntoa(cli_addr.sin_addr),ntohs(cli_addr.sin_port));
        send(newfd,buf,strlen(buf),0);
    }
}
int main(){
    int fd=socket(AF_INET,SOCK_STREAM,0);
    if(fd<0){
        perror("socket");
        return 0;
    }
    struct sockaddr_in addr;
    addr.sin_family=AF_INET;
    addr.sin_port=htons(9999);
    addr.sin_addr.s_addr=inet_addr("0.0.0.0");
    int ret=bind(fd,(struct sockaddr*)&addr,sizeof(addr));
    if(ret<0){
        perror("bind");
        return 0;
    }
    listen(fd,5);
    sockaddr_in cli_addr;
    socklen_t cli_len=sizeof(cli_addr);
    while(1){
        int newfd=accept(fd,(sockaddr*)&cli_addr,&cli_len);
        if(newfd<0){
            perror("accept");
            return 0;
        }
        Cli_info* cli=new Cli_info;
        cli->newfd=newfd;//保存新连接套接字描述符,交给工作线程使用
        memcpy(&cli->cli_addr,&cli_addr,sizeof(cli_addr));//保存客户端地址信息,交给工作线程使用
        pthread_t thread; 
        int ret=pthread_create(&thread,NULL,start,(void*)cli);//创建工作线程,并传递相应的数据
        if(ret<0){
            close(newfd);
            delete cli;
        }
    }
    return 0;
}

编译运行,测试:

查看serve进程与线程:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值