C++项目实战-socket编程

目录

socket套接字概念

字节序

网络字节序

socket地址

通用socket地址

专用socket地址

IP转换址转换函数 

网络套接字函数

socket模型创建流程图(TCP通信流程/CS模型流程图)

出错处理封装函数


socket套接字概念

        所谓套接字,就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。

一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进程通信的接口,是应用程序与网络协议进行交互的接口。

        它是网络环境中进行通信的API,使用中每一个套接字都有一个与之相连进程。通信时其中一个网络应用程序将要传输的一段信息写入他所在的主机socket中,该socket通过与网络接口卡(NIC)相连的传输介质将这段信息送到另一台主机的socket中,使对方能够接收到这段信息socket是由IP地址和端口结合的,提供应用层进程传送数据包的机制。

        socket本意上“插座”的意思,在Linux环境中,用于表示进程间网络通信的特殊文件类型。本质上为内核借助缓冲区形成的伪文件。把它设置为文件,方便我们进行操作,我们可以通过文件描述符进行操作。与管道类型,Linux系统将期封装成文件的目的是为了统一接口,使得读写套接字和读写文件操作一样。区别是管道应用于本地进程间通信,而套接字多用于网络进程间数据的传递。

        socket是全双工通信,即在同一时刻既可以数据读入,也可以数据输出。

MAC地址(物理地址)

IP地址(逻辑地址): 在网络中唯一标识一台主机

端口号:在一台主机中唯一标识一个进程

IP+端口号:在网络环境中唯一标识一个进程

       

套接字原理:(绑定了IP和端口号)

在网络中套接字一定是成对出现的。

TCP/IP协议最早在BSD UNIX上实现,为TCP/IP协议设计的应用层编程接口称为socket API。 

-服务器端:被动接受连接,一般不会主动发起连接

-客户端:主动向服务器发起连接     

字节序

现在CPU的累加器一次都能装载(至少)4个字节(32位机),即一个整数。哈哈,我在想32位下指针也是4个字节,设定指针的大小与能够寻址范围有关,累加器的装载量限制了指针的大小和寻址范围。那么这4个字节在内存中排列的顺序将影响它被累加器装载的整数值,这就是字节序问题。在各种计算机体系结构中,对于字节、字等的存储机制有所不同,因而引发了计算机通信领域中一个很重要的问题,即通信双方交流的信息单元应该以什么样的顺序进行传送。如果不达成一致的规则,通信双方将无法进行正确的编码/译码从而导致通信失败。

(总而言之:字节序就是存储数据的方式,大家按照统一的规则进行,保证数据传输的正确性)

字节序分为大端字节序(Big-Endian)和小端字节序(Little-Endian)。大端字节序是指一个整数的高位字节存储在内存的低地址位置,低位字节存储在内存的高地址位置。小端字节序则是指一个整数的高位字节存储在内存高地址处,而低位字节则存储在内存的低地址处。

大端:低地址---高位

小端:高地址---低位

记忆方法: 低址对高位为大   高址对低位为小

网络字节序

        当格式化的数据在两台使用不同字节序的主机之间直接传递时,接收端必然会解释错误。那么怎么解决这个问题呢?假设我们让发送数据的一端总是以大端字节序发送(对就是做一个统一的规定),那么接收数据的一端就知道我接受到的字节序总是大端字节序,如果接受方的字节序为小端,那么只需要大端数据装换成小端字节序就可以了。

        TCP/IP协议中规定,网络数据流应采用大端字节序。

        

为了使用方便和移植性,我们可以调用相应的库函数进行转换...

h - host 主机,主机字节序

to 转换成什么

n - network 网络字节序

s - short unsigned short                端口

l - long unsigned int                       IP

#include <arpa/inet.h>

// 转换端口

uint16_t htons(uint16_t hostshort);                // 主机字节序 - 网络字节序

uint16_t ntohs(uint16_t netshort);                  // 主机字节序 - 网络字节序

// 转IP

uint32_t htonl(uint32_t hostlong);                   // 主机字节序 - 网络字节序

uint32_t ntohl(uint32_t netlong);                    // 主机字节序 - 网络字节序

测试下我本机的字节序吧,嘻嘻

#include <stdio.h>
#include <arpa/inet.h>

//先定义一个联合体
union
{
    int     number;
    char    c;
}test;

//为什么用这个联合体可以测试呢?
/*
    联合体:所有变量共用一块内存
        按最大的成员变量进行申请内存
        每一时刻只能有一个成员
    
    对于test:
        siezof(test) = 4
        如果用 c  = 1 type   ==> 每次都能够取到最低位置
*/


int main(void)
{

    test.number = 0x12345678;
    if(test.c == 0x12)  //高位存储在内存的低地址上
    {
        printf("本机为大端字节序\n");
    }
    else
    {
        printf("本机为小端字节序\n");
    }



    return 0;
}

socket地址

        socket地址其实是一个结构体,封装端口和IP等信息。后面的socket相关的api中需要使用到这个socket地址。

        之前有说到过,socket套接字上联应用程序,下联协议栈

        对一个一个数据包想要在网络中的两台不同主机间的进程(当然我们这里不包括本地套接字),只要确认了对方IP(逻辑地址)和端口就可以将数据传送给对方【MAC地址可以根据ARP协议获取到】

通用socket地址

socket网络编程接口中表示socket地址是结构体sockaddr,其定义如下:

#include <bits/socket.h>

struct sockaddr{                                //已经被废弃掉

        sa_family_t         sa_family;

        char                    sa_data[14];

};

typedef unsigned short int sa_family_t;

成员:

        sa_family成员是地址族类型(sa_family_t)的变量。地址族类型通常与协议类型对应。常见的协议族和对应的地址族如下所示:

协议族地址族描述
PF_UNIXAF_UNIXUNIX本地域协议族
PF_INETAF_INETTCP/IPv4协议族
PF_INET6AF_INET6TCP/IPv6协议族

协议族 PF_*和地址族AF_*在头文件bits/socket.h中,二者值相同,可以混合使用(反正都是宏定义,宏定义是预处理阶段进行宏替换,所以混着用对编译运行不会有影响)

其实我们很容易看到一个问题,

这个地方使用的是一个固定数  14type:

sa_data成员用于存放socket地址值。但是,不同的协议族的地址值具有不同的含义和长度

我们可以看到,14个字节几乎只能装下 IPv4地址。因此,Linux定义了下面这个新的通用的socket地址结构体,这个结构体不仅提供了足够大的空间用于存放地址值,而且是内存对齐的【内存对齐可以加快CPU访问速度,内存对齐问题见我的C语言专栏,有详细介绍】 

这个结构体定义在:/usr/include/linux/in.h

为了方便理解,我们去掉一些杂乱的信息:

#include <bits/socket.h>
struct sockaddr_storage
{
sa_family_t sa_family;
unsigned long int __ss_align;
char __ss_padding[ 128 - sizeof(__ss_align) ];
};
typedef unsigned short int sa_family_t;

专用socket地址

很多网络编程函数诞生早于IPv4协议(用自定义的协议咯,双方共同约定一个规则),那时候都是使用struck socketaddr结构体,为了向前兼容,现在在socketaddr退化成了 (void *)的作用,传递一个地址给函数,至于这个函数是sockaddr_in还是sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型

 UNIX 本地域协议族使用如下专用的 socket 地址结构体:

#include <sys/un.h>
struct sockaddr_un
{
sa_family_t sin_family;
char sun_path[108];
};

TCP/IP 协议族有 sockaddr_in 和 sockaddr_in6 两个专用的 socket 地址结构体,它们分别用于 IPv4 和 IPv6:

#include <netinet/in.h>
struct sockaddr_in
{
sa_family_t sin_family; /* __SOCKADDR_COMMON(sin_) */
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) - sizeof (struct in_addr)];
};
struct in_addr
{
in_addr_t s_addr;
};
struct sockaddr_in6
{
sa_family_t sin6_family;
in_port_t sin6_port; /* Transport layer port # */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* IPv6 scope-id */
};
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef uint16_t in_port_t;
typedef uint32_t in_addr_t;
#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))

所有专用 socket 地址(以及 sockaddr_storage)类型的变量在实际使用时都需要转化为通用 socket 地 址类型 sockaddr(强制转化即可),因为所有 socket 编程接口使用的地址参数类型都是 sockaddr。

IP转换址转换函数 

人们习惯用可读性好的字符串来表示IP地址,比如用点分十进制字符串表示IPV4地址,以及用十六进制字符串表示IPv6地址,但编程中我们需要先把他们转化为整数(二进制)方能使用。而记录日志相反,我们需要把整数表示的IP地址转化为可读的字符串。

早期:(不推荐使用)

#include <sys/socket.h>

#include <netinet/in.h>

#include <arpa/inet.h>

int inet_aton(const char *cp,struct in_addr *inp);

in_addr_t  inet_addr(const char *cp);

char *inet_ntoa(struct in_addr in);         

这只能处理IPV4的ip地址,不可重入函数

现在:

p:点分十进制的IP字符串

n:表示network,网络字节序的整数

#include  <arpa/inet.h>

int inet_pton(int af,const char *src,void *dst);

af:地址族: AF_INET AF_INET6

src:需要转换的点分十进制的IP字符串

dst:转换后的结果保存在这个里面

const char *inet_ntop(int af,const void *src,char *dst,socklen_t size);

af:AF_INET   AF_INE6

src: 要转换的ip的整数的地址

dst: 转换成IP地址字符串保存的地方

size:第三个参数的大小(数组的大小)

返回值:返回转换后的数据的地址(字符串),和 dst 是一样的

点分十进制 --->  网络字节序   inet_pton

网络字节序 --->  点分十进制   inet_ntop

网络套接字函数

socket模型创建流程图(TCP通信流程/CS模型流程图)

头文件:#include <arpa/inet.h>

或者:#include <sys/types/h>    #include <sys/socket.h>

int socket(int domain,int type,int protocol);

功能:创建一个套接字

参数:

        domain:协议族

                AF_INET   -->  ipv4

                AF_INET6 -->  ipv6

                AF_UNIX  AF_LOCAL  --> 本地套接字通信(进程间通信)

        type:通信过程中使用的协议协议

                SOCK_STREAM --> 流式协议

                SOCK_DGRAM   --> 报式文件

        protocol:具体的一个协议,一般写 0

                SOCK_STREAM  --> 流式文件默认使用 TCP

                SOCK_DGRAM    --> 报式文件默认使用 UDP

返回值:

        成功:返回文件描述符,操作的就是内核缓冲区(socket本质上是一个伪文件)

        失败:-1

int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);

功能:绑定,将fd和本地的IP+端口进行绑定

参数:

            sockfd:通过socket函数得到的文件描述符

            addr:需要绑定的socket地址,这个地址封装了ip和端口号的信息

            addrlen:第二个参数结构体占的内存大小

int listen(int sockfd,int backlog);

功能:监听这个socket上的连接

参数:

           sockfd:通过socket()函数得到的文件描述符

            backlog:未连接的和、已连接的和的最大值        5

int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);

功能:接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接

参数:

           sockfd:用于监听的文件描述符

           addr:传出参数,记录连接成功后客户端的地址信息(ip、port)

           addrlen:指定第二个参数的对应的内存大小

返回值:

        成功:用于通信的文件描述符

        失败:-1

ssize_t write(int fd, const void *buf, size_t count); // 写数据

ssize_t read(int fd, void *buf, size_t count); // 读数据

上个程序案例

#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
//定义IP
#define SERVER_IP   "127.0.0.1"
//定义端口
#define SERVER_PORT  8080

int main(void)
{

    int lfd,cfd;
    char str[INET_ADDRSTRLEN];
    //创建socket套接字
    lfd = socket(AF_INET,SOCK_STREAM,0);  //TCP  ipv4
    //绑定 IP(server) 和 端口号(监听)
    struct sockaddr_in serverAddr;
    memset(&serverAddr,0,sizeof(serverAddr));
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(SERVER_PORT);
    serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);  //或者 INADDR_ANY:提供任意一个本地有效IP
    bind(lfd,(struct sockaddr *)&serverAddr,sizeof(serverAddr));
    //监听 设置最大监听数目 128
    listen(lfd,128);
    //等待连接
    struct sockaddr_in clientAddr;
    socklen_t clientAddr_len = sizeof(clientAddr);
    cfd = accept(lfd,(struct sockaddr *)&clientAddr,&clientAddr_len); 
    //数据交换
    int n,i=0;
    char buf[1024] = {0};
    while(1)
    {     
        n = read(cfd,buf,sizeof(buf));
        if(n == 0)  //有客户端断开连接
        {
            printf("有客户端断开连接\n");
        }
        if(n < 0)
        {
            printf("aaaaaaaa\n");
        }
        // inet_ntop(AF_INET,&clientAddr.sin_addr,str,sizeof(str));
        // ntohs(clientAddr.sin_port);
        printf("已收到第%d次数据:%s\n",i++,buf);
        //sleep(2);
        write(cfd,buf,n);
    }   

    close(cfd);
    close(lfd);
    return 0;
}
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>

//定义IP
#define SERVER_IP   "127.0.0.1"
// //定义端口
#define SERVER_PORT  8080

int main(void)
{

    int sockfd;
    //创建套接字  TCP ipv4
    sockfd = socket(AF_INET,SOCK_STREAM,0); 
    //连接
    struct sockaddr_in serverAddr;
    memset(&serverAddr,0,sizeof(serverAddr));
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(SERVER_PORT);
    inet_pton(AF_INET,SERVER_IP,&serverAddr.sin_addr);
    connect(sockfd,(struct sockaddr *)&serverAddr,sizeof(serverAddr));
    //数据交换
    char buf[1024] = {0};
    int i=0,n=0;
    while(1)
    {
        //memset(buf,0,sizeof(buf));
        fgets(buf,sizeof(buf),stdin);
        //scanf("%s",buf);
        write(sockfd,buf,sizeof(buf));
        memset(buf,0,sizeof(buf));
        n = read(sockfd,buf,sizeof(buf));
        printf("------a-------\n");
        write(STDOUT_FILENO,buf,n);
    }

    close(sockfd);

    return 0;
}

客户端与服务器端启动连接后可以使用 netstat -apn|grep 8080查看连接情况

出错处理封装函数

        我们知道,系统调用不能保证每次都成功,必须进行错误处理,这样一方面可以保证程序的逻辑正常,另一方面可以迅速得到故障信息。

        为使错误处理的代码不影响主程序的可读性,我们把与socket相关的一系列函数加上错误代码封装成新的函数(按照系统的库函数进行封装),做成一个模板 wrap.c

 如果有需要,请把这份代码自己保存好,下次可以直接使用.(有兴趣的可以打包成动态库)

        

先来把头文件搞定:

先来看下我们需要定义哪些东西:其实很简单,其实并不难。先把框架搭起来。欧里给

对呀,就上面这些系统调用,直接粘过来,把每一个都一个错误处理的接口API.  搞定

函数名直接取,最好按驼峰法取名

#ifndef _WRAP_H_
#define _WRAP_H_

void perr_exit(const char *s);
int  Accept(int fd,struct sockaddr *sa,socklen_t *salenptr);
int  Bind(int fd,const struct sockaddr *sa,socklen_t salen);
int  Connect(int fd,const struct sockaddr *sa,socklen_t salen);
int  Listen(int fd,int backlog);
int  Socket(int family,int type,int protocol);
ssize_t Read(int fd,void *ptr,size_t nbytes);
ssize_t Write(int fd,const void *ptr,size_t nbytes);
int Close(int fd);
ssize_t Readn(int fd,void *vptr,size_t n);
ssize_t Writen(int fd,const void *vptr,size_t n);
ssize_t my_read(int fd,char *ptr);
ssize_t Readline(int fd,void *vptr,size_t maxlen); 

#endif

 

#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <error.h>

void perr_exit(const char *s)
{
    perror(s);
    exit(1);
}
int  Accept(int fd,struct sockaddr *sa,socklen_t *salenptr)
{
    int n; 
    //accept:阻塞,是慢系统调用。可能会被信息中断
    again:
    if((n = accept(fd,sa,salenptr)) < 0)
    {
        if((errno == ECONNABORTED) || (errno == EINTR))
        {
            goto again;   //重启
        }
        else
        {
            perr_exit("accept error");
        }
    }
    return n;
}
int  Bind(int fd,const struct sockaddr *sa,socklen_t salen)
{
    int n;
    if((n = bind(fd,sa,salen)) < 0)
    {
        perr_exit("bind error");
    }
    return n;
}
int  Connect(int fd,const struct sockaddr *sa,socklen_t salen)
{
    int n;
    if((n = connect(fd,sa,salen)) < 0)
    {
        perr_exit("connect error");
    }
    return n;
}
int  Listen(int fd,int backlog)
{
    int n;
    if((n = listen(fd,backlog)) < 0)
    {
        perr_exit("listen error");
    }
    return n;
}
int  Socket(int family,int type,int protocol)
{
    int n;
    if((n = socket(family,type,protocol)) < 0)
    {
        perr_exit("socket error");
    }
    return n;
}
ssize_t Read(int fd,void *ptr,size_t nbytes)
{
    ssize_t n;
    again:
    if((n = read(fd,ptr,nbytes)) == -1)
    {
        if(errno == EINTR)//被中断
        {
            goto again;
        }
        else
        {
            return -1;
        }
    }
    return n;
}
ssize_t Write(int fd,const void *ptr,size_t nbytes)
{
    ssize_t n;
    again:
    if((n = write(fd,ptr,nbytes)) == -1)
    {
        if(errno == EINTR)
        {
            goto again;
        }
        else
        {
            return -1;
        }
    }
    return n;
}
int Close(int fd)
{
    int n;
    if((n = close(fd)) == -1)
    {
        perr_exit("close error");
    }
    return n;
}
ssize_t Readn(int fd,void *vptr,size_t n)
{
    size_t nleft;
    ssize_t nread;
    char *ptr;
    ptr = vptr;
    nleft = n;

    while(nleft > 0)
    {
        if((nleft = read(fd,ptr,nleft)) < 0)
        {
           if(errno == EINTR)
           {
                nread = 0;
           }
           else
           {
                return -1;
           }
        }
        else if(nread == 0)
        {
            break;
        }
        nleft -= nread;
        ptr += nread;
    }
    return n-nleft;

}
ssize_t Writen(int fd,const void *vptr,size_t n)
{
    size_t nleft;
    ssize_t nwritten;
    const char *ptr;
    ptr = vptr;
    nleft = n;

    while(nleft > 0)
    {
        if((nwritten = write(fd,ptr,nleft)) <= 0)
        {
            if(nwritten < 0 && errno == EINTR)
            {
                nwritten = 0;
            }
            else
            {
                return -1;
            }
        }
        nleft -= nwritten;
        ptr += nwritten;
    }
    return n;
}
static ssize_t my_read(int fd,char *ptr)
{
    static int read_cnt;
    static char *read_ptr;
    static char read_buf[100];

    if(read_cnt <= 0)
    {
        again:
            if((read_cnt = read(fd,read_buf,sizeof(read_buf))) < 0)
            {
                if(errno == EINTR)
                {
                    goto again;
                }
                return -1;
            }
            else if(read_cnt == 0)
            {
                return 0;
            }
            read_ptr = read_buf;
    }
    read_cnt--;
    *ptr = *read_ptr++;
    return 1;
}
ssize_t Readline(int fd,void *vptr,size_t maxlen)
{
    ssize_t n,rc;
    char c,*ptr;
    ptr = vptr;

    for(n=1;n<maxlen;n++)
    {
        if((rc = my_read(fd,&c)) == 1)
        {
            *ptr++ = c;
            if(c == '\n')
            {
                break;
            }
        }
        else if(rc == 0)
        {
            *ptr = 0;
            return n-1;
        }
        else
        {
            return -1;
        }
    }
    *ptr = 0;
    return n;
} 

     

  • 20
    点赞
  • 144
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
### 回答1: "c 2017网络编程实战 pdf" 是一个网络编程实战的PDF教程资源,适用于C语言编程方向的学习者。该资源提供了关于网络编程的实际操作和实战案例,并且以PDF的形式呈现,方便学习者离线阅读和学习。 网络编程是指通过计算机网络进行数据传输和通信的一种编程技术。在现代互联网时代,网络编程技术的应用非常广泛,如网站的开发、网络游戏的开发、通信软件的开发等。掌握网络编程技术,是提高软件开发水平和获取一些高级工程师职位的必备技能之一。 "c 2017网络编程实战 pdf" 应该包含了网络编程的基础知识和实践案例。学习者可以通过学习这本书来了解网络编程的基本原理,熟悉网络编程的相关术语和模型,掌握网络编程的常用技术和工具,并通过实际操作来加深对网络编程的理解。 此外,这本书可能还会提供一些网络编程的实战案例,例如通过Socket API实现一个简单的网络聊天室、使用HTTP协议进行数据传输等。这些实战案例可以帮助学习者将理论知识转化为实际应用能力,并提供一些实践经验和技巧。 总之,"c 2017网络编程实战 pdf" 是一本适合学习C语言网络编程的教程资源,提供了网络编程的基础知识、实践案例和操作指引,对于想要学习网络编程的C语言学习者是一本宝贵的参考资料。 ### 回答2: c2017网络编程实战 PDF是一本关于网络编程的实用指南。网络编程是现代软件开发中非常重要的一部分,它涉及到利用计算机网络进行数据传输和通信的技术和方法。 这本书的目的是帮助读者学习和掌握网络编程的基本原理和技术。书中详细介绍了如何使用C语言进行网络编程,包括套接字编程、网络协议和网络通信等内容。通过实战案例的讲解,读者可以深入了解网络编程的实际应用和实践技巧。 随着互联网的蓬勃发展,网络编程在各个行业和领域都有广泛的应用。掌握网络编程技术可以帮助开发者设计和实现高效的网络应用,实现数据的快速传输和实时通信。尤其是在移动互联网和物联网领域,网络编程的需求更加迫切。 本书不仅针对初学者,还适合有一定编程基础的读者。通过学习网络编程,读者可以提升自己的编程能力,并且能够应对各种网络编程问题和挑战。 总之,《c2017网络编程实战PDF》是一本很好的学习资源,它为读者提供了基础知识和实用技巧,帮助他们在现代软件开发中掌握网络编程的核心概念和技术。阅读这本书将为读者打开网络编程的大门,并为未来的学习和实践奠定坚实的基础。 ### 回答3: 《C 2017网络编程实战》是一本关于网络编程的实用指南。网络编程是一种在计算机网络上进行数据传输和通信的技术,对于计算机科学和软件工程领域的学习者和专业人士来说具有重要意义。 这本书通过介绍C语言的网络编程库和相关知识,帮助读者了解网络编程的基本原理、网络模型、数据传输和协议等内容。书中提供了大量实例和示例代码,可以帮助读者快速掌握网络编程的技巧和方法。 此外,本书还详细介绍了网络编程中常用的Socket编程TCP/IP、UDP、HTTP等协议,以及多线程和并发编程等技术。通过学习这些内容,读者可以理解网络通信的原理和机制,掌握网络编程的基本技能。 《C 2017网络编程实战》适合计算机专业学生、软件工程师和对网络编程感兴趣的人士阅读。无论是想深入了解网络编程的原理,还是希望掌握实际应用网络编程的技能,这本书都能够提供很好的指导和帮助。 总之,通过阅读《C 2017网络编程实战》,读者可以系统地学习和掌握网络编程的基本知识和技能,为日后在网络开发和应用方面的工作打下坚实的基础。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

@陈一言

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值