Linux C语言编程-Linux网络通信--Linux上使用套接字(socket)来发送信息---知识点总结+实例

Linux上C程序开发 专栏收录该内容
5 篇文章 0 订阅

*********************注意:为了保证文章的完整性和全面性,作者会不定期对文章进行更新和修正*********************

*相关软件的版本:

gcc版本:gcc (GCC) 4.1.2 20080704 (Red Hat 4.1.2-52)

1.Socket相关概念:
(1)套接字是什么?
    套接字是一种通信机制这种通信机制不仅可以在网络间使用,还可以在本机上使用,它由源地址IP和目标地址IP还有源端口和目标端口组成,套接字具体表现为由Linux系统内核提供的一系列函数接口(API),在这组函数接口的后面隐藏了TCP/IP等复杂的底层协议和网络中数据传输的一些复杂的操作,我们编写的程序或是一些线程的软件可以通过调用套接字来完成网络间的通信(例如远程登录,和ftp文件传输都是使用套接字来完成的),所以你还可以将套接字想成为一个中间件,应用程序通过使用这个中间件完成和不同机器的网络的通信。
    注意:套接字并不是我们一般意义上的软件,很明显,我们不能通过命令来直接调用套接字完成我们想要的功能。

(2)Linux创建套接字和套接字连接的过程
    首先套接字分为两个部分,一个是服务器端,一个是客户端
    服务端:首先服务器端的应用程序会调用套接字的相关AIP来创建一个套接字,那么这个套接字在Linux上具体表现为一个特殊的文件(这个文件一般放置在/tmp目录下面,在Linux上东西基本上都为文件),之后会为这个套接字起个名字,而程序是以类似于文件描述符的形式来调用这个套接字,之后服务端就可以等待客户端来连接这个套接字,连接过来接受就可以了,从而建立一个完整的套接字来进行信息传输。
    注意:套接字是不能在进程之间分享的,意思就是说两个不同的进程不能使用其他进程创建的套接字
    客户端:同理客户端也是通过应用程序来调用相关接口创建一个套接字,并且给这个套接字来命名,之后使用这个套接字来连接服务端的套接字,从而建立一个完整的套接字连接,来传输信息。


2.Socket的属性
*首先介绍一些协议:
TCP/IP协议:TCP协议(传输控制协议),IP协议(网际协议),TCP协议在传输数据主要负责数据在接受或是传输时候的排序,控制,重发等等,他的目的就是为了保证发出去的数据和接收到的数据保持一致以及正确,IP协议是一个底层协议,他的主要作用就是发送数据,IP要管理链路的建立,以及发送数据时路由的跳转,十分复杂

套接字有三个属性:域,类型,协议
    (1)域:这个属性用来指定套接字通信中使用的网络介质,换句话说这个属性用来定义套接字使用的是哪一种网络,不同的域上使用的网络的协议也是不同的。
        套接字使用的相关域:
            <1>AF_UNIX:UNIX域协议
            <2>AF_INET:ARAP因特网协议(其实就是IPv4协议,IPv6好像是AF_INET6)
            <3>AF_ISO:ISO标准协议
            <4>AF_NS:施乐网络系统协议
            <5>AF_IPX:Novell IPX协议
            <6>AF_APPLETALK:Appletalk DDS

(2)类型:在一个网络域中使用套接字发送信息的方式是不同的,这个方式就由类型来定,在Linux上套接字有两种类型:流套接字,数据报套接字。
        <1>流套接字(sock_stream):流套接字是在AF_INET上使用的,并且通过TCP/IP协议实现的,流套接字提供的是有序,可靠的,双向的字节流连接,这种方式可以使得数据在传输的时候不丢失,而且流套接字传输数据是没有长度限制的,复制或是乱序,所以流套接字的使用十分的常见,但是相对的流套接字也要消耗系统更多的资源。
        其实对于流套接字,你可以将其想象为一个严格监控的高速公路,这个高速公路可以双向流通数据,因为监控严格上面的数据也十分安全,但是高速公路维护起来成本也高
        <2>数据报套接字(sock_dgram):数据报套接字也是在AF_INET上使用的,但是这个数据报使用的是UDP/IP协议,UDP协议名字为“用户数据报协议”,这个协议和TCP协议的区别在于,UDP不会去维持一个链路,我们知道TCP在进行网络通信的时候会在发送数据的机器和接受数据的机器之间建立一个链路,这要这个链路建立成功就不会断开,就算你不传输数据它也不会断开,除非你终止程序,或者网络出现了问题。但是UDP不同,它不会一直去维持这个链路,只要有数据要发送他就创建链路,发送完了就断开,所以以UDP协议为基础的数据报套接字消耗的资源少,但是数据稳定,出错的几率也十分高。
        <3>原始套接字(sock_raw):这里不进行过多的解释,但是你要明白流套接字只能读取TCP协议层给出的数据,而数据报套接字只能读取UDP协议层给出的数据,但是原始层套接字可以读取更底层协议的数据比如IP协议层。

(3)协议:一般来讲我们都不用讨论协议,因为我们在使用C在Linux上创建套接字的时候,只要有域和类型系统就足以判断你要使用的协议了,系统会帮你完成的。


3.创建服务端的套接字的流程


(1)创建一个服务端的套接字的流程:
    <1>创建一个套接字。
    <2>设置一个套接字的命名结构体的相关参数,并使用哪个命名结构体给套接字命名
    <3>给这个套接字设置一些相关的属性
    <4>开始监听套接字的连接
    <5>接受套接字的连接
    <6>开始处理连接
    <7>关闭连接好的套接字

(2)创建一个客户端套接字的流程:
    <1>创建一个套接字。
    <2>设置一个套接字的命名结构体的相关参数,但是不需要绑定命名
    <3>给这个套接字设置一些相关的属性
    <4>连接服务端套接字
    <5>开始操作连接好的套接字
    <6>关闭套接字


4.套接字相关的函数

(1) #include <sys/types.h>
    #include <sys/socket.h>
    int socket(int domain, int type, int protocol)
函数作用:用来创建一个没有命名的套接字,并且返回它的文件描述符
参数:1.domain:这个参数指定的是套接字使用的域,可以使用的值为:AF_INET, AF_UNIX, AF_ISO, AF_NS, AF_IPX, AF_APPLETALK,针对于IPv4的因特网,我们一般使用AF_INET
    2.type:这个参数指定的是socket的类型,就像我们之前说的socket的类型有流套接字,和数据报套接字,这个参数可以使用的值为:SOCK_STREAM(流), SOCK_DGAM(数据报)
    3.protocol:这个参数是套接字使用的协议,可以使用的参数为:IPPROTO_TCP,IPPROTO_UDP,IPPROTO_STCP,IPPROTO_TIPC(分别对应TCP, UDP, STCP,TIPC),但是如果damin和type这两个数据定义下来,系统就会判断出使用的protocol,并且它会默认帮你指定使用的协议,所以只要前两个参数定下来了,第三个参数可以使用0
返回值:1.成功:返回套接字的文件描述符,这个文件描述符是int类型的
    2.失败:返回-1,并且设置errno错误代码

(2) #include <sys/socket.h>
    int bind(int socket, const struct sockaddr* address, size_t address_len)
相关知识点:1.我们说每个套接字域都有自己的地址格式,这些地址格式我们一般使用结构体来描述
    先介绍两个结构体:
    struct sockadd_in
    {
        short int sin_family;    //使用的协议域,这个值为AF_INET
        unsigned short int sin_port;    //保存使用的端口
        struct in_addr sin_addr;    //IP地址
    }
    这个结构体保存的是AF_INET网域(IPv4,因特网)使用的地址相关的数据

    struct in_addr
    {
        unsigned long int s_addr;    //地址其实是long int类型数据
    }
函数作用:这个bind函数的作用是给套接字进行绑定命名,使得套接字关联到一个IP地址和端口号
参数:1.socket:我们要进行绑定操作的套接字
    2.address:保存地址信息的结构体
    3.address_len:address指向的结构体的大小
返回值:1.成功:返回0
    2.失败:返回-1,并且设置errno,可以通过stderror来得到错误的具体信息
    errno有如下几个错误(AF_INET域中的):
        1.EBADF(文件描述符无效)
        2.ENOTSOCK(文件描述符对应的不是一个套接字)
        3.EINVAL(文件描述符对应的是一个已命名的套接字)
        4.EADDRNOTAVAIL(地址不可使用)
        5.EADDRINUSE(地址已经绑定了一个套接字)

(3) #include <sys/socket.h>
    int listen(int socket, int backlog)
函数作用:这个Listen函数的作用是为套接字申请一个连接的队列,并且为我们的套接字创建接口和建立连接的后备日志使得我们创建的套接字可以被连接。
参数:1.socket:我们要建立监听队列的套接字
    2.backlog:套接字监听队列的长度
返回值:1.成功:函数执行成功的时候返回0
    2.失败:函数执行失败的时候返回-1,并且还会设置errno,errno有如下几个错误(AF_INET域中的):
        1.EBADF(文件描述符无效)
        2.EINVAL(文件描述符对应的是一个已命名的套接字)
        3.ENOTSOCK(文件描述符对应的不是一个套接字)

(4) #include <sys/socket.h>
    int accept(int socket, struct sockaddr* address, size_t* address_len)
函数作用:这个accept函数的作用就是就是接受网络上其他机器连接过来的套接字,其实当有套接字要连接服务器的时候,连接是先到套接字队列里面去的,当套接字的连接队列里面有没有处理的套接字的时候accpet函数就会去处理这个套接字,accept函数会创建一个新的套接字,并使用这个套接字和队列中的套接字进行通信并且返回它自己新建立的套接字,用户可以使用这个套接字来得到数据。
参数:1.socket:服务端的套接字,也就是其他客户机器要连接的套接字
    2.address:这个地址结构体是用来保存客户端的连接的地址的信息,我们知道我们编写套接字的客户端的时候要建立一个
    地址结构体来保存客户端要连接的地址,而这个address保存的就是那个客户端连接的地址信息,注意请一定区分连接地址
    信息和客户端地址信息,如果你对这个参数不感兴趣可以将其设置为空指针
    4.address_len:这个参数是由服务端设置的,当我们接收的地址的长度大于address_len的时候,客户端的地址将被截断
返回值:1.成功:这个函数成功时会返回新的套接字的文件描述符
    2.失败:函数执行失败的时候返回-1,并且设置errno,可以通过stderror来得到错误的具体信息。
*设置的错误的编码
    WSANOTINITIALISED:在使用此API之前应首先成功地调用WSAStartup()。
    WSAENETDOWN:套接口实现检测到网络子系统失效。
    WSAEFAULT:addrlen参数太小(小于socket结构的大小)。
    WSAEINTR:通过一个WSACancelBlockingCall()来取消一个(阻塞的)调用。
    WSAEINPROGRESS:一个阻塞的套接口调用正在运行中。
    WSAEINVAL:在accept()前未激活listen()。
    WSAEMFILE:调用accept()时队列为空,无可用的描述字。
    WSAENOBUFS:无可用缓冲区空间。
    WSAENOTSOCK:描述字不是一个套接口。
    WSAEOPNOTSUPP:该套接口类型不支持面向连接服务。
    WSAEWOULDBLOCK:该套接口为非阻塞方式且无连接可供接受。
    WSAECONNRESET:接受连接后在accept返回之前,被远程客户端断开连接。
*函数注意事项
(1)这个函数在套接字队列里面没有等待处理的套接字的时候,这个函数就会阻塞当前程序,阻塞的意思就是说当前的程序会停止在这个函数上。
(2)我们可以通过fcntl()函数来改变这个情况,fcntl()函数可以通过文件描述符,来改变文件描述符的性质,我们说过套接字也是文件
    int flags = fcntl(socket, F_GETFL, 0)
    fcntl(socket, F_SETFL, O_NONBLOCK|flags)
通过以上的代码我们将我们的创建socket设置为了非阻塞模式,也就是说当没有连接这个套接字的客户端的时候,accept()会马上返回

(5) #include <sys/socket.h>
    int connect(int socket, const struct sockaddr* address, size_t address_len)
函数作用:这个函数是客户端使用的,它的作用就是将本地套接字连接到远程服务端上的一个套接字上,从而可一和服务端进行通信
参数:1.socket:我们本地客户端建立的套接字

    2.address:这个地址结构体用来保存我们要连接的服务端的地址信息

    3.address_len:指定address结构体的大小
返回值:1.成功:这个函数返回0
    2.失败:这个函数返回-1,并且设置errno,可以通过stderror来得到错误的具体信息
    错误代码列表
        1.EBADF:传递给socket参数的文件描述符无效
        2.EALREADY:改套接字上已经有一个正在进行中的连接
        3.ETIMEDOUT:连接超时
        4.ECONNERFUSED:连接请求服务器拒绝
*函数注意事项
(1)如果connect()函数不能立刻和服务端的连接,connect()函数会阻塞一段不确定的时间,这个时间之内connect()函数会不断的尝试连接远程服务端,如果说在这段时间内还是没有连接上服务端的话就直接返回-1,并且将errno设置为ETIMEDOUT

(6) #include <netinet/in.h>
    unsigned long int htonl(unsigned long int hostlong)
函数作用:这个函数的作用是将你创建套接字所使用的IP地址或是端口号或是将一般数据转换为网络字节序
参数:1. hostlong:你创建套接字是要使用的IP地址
返回值:1.成功:函数执行成功的时候返回转换为网络字节序的数据
*函数注意事项
(1)为什么要将数据转化为网络字节序:我们在创建套接字服务端或是客户端的时候都逃离不了硬件的制约,也就是说我们创建套接字的时候总是要使用硬件的,但是根据CPU的不同会导致我们在创建套接字映射到硬件上的时候我们创建的套接字会产生变化,这是因为CPU处理的字节序不同产生的,比如你在代码中写入的套接字的端口是1574,结果映射到硬件上就变成了9743,这会使得我们建立的套接字失去原本的作用,所以为了让这些数据有一致性,我们就将其转化为一个统一的序列显示,这个序列就是网络字节序列,转化为网络字节序列的数据映射到硬盘上是不会改变的

(2)其他相关函数:
*首先htonl = host to network long
1.unsigned short int htons(unsigned short int hostshort):这个函数和htonl()的效果是一样的,只不过它是将数据转为网络字节序的短整型而htonl是将数据转化为长整形,htons = host to network short
2.unsigned long int ntohl(unsigned long int netlong):这个函数的作用是将网络字节序转化为本地字节序,这个函数是将网络字节序转化为本地字节序的长整形,network to host long
3.unsigned short int ntohs(unsigned short int netshort):这个函数的作用就是将网络字节序列转化为本地字节序列,它会将本地字节序列转化
短整形,network to host short
4.#include <arpa/inet.h>
  char* inet_ntoa(struct in_addr in):这个函数的参数为一个in_addr结构体,这个结构体就是我们所说的网络字节序,这个inet_ntoa()函数的作用就是将一个网络字节序变为一个字符串类型,有"."做分割的IP地址

(7) #include <netdb.h>
    struct hostent* gethostbyaddr(const void* addr, size_t len, int type)
函数作用:这个函数的作用是通过addr这个指针保存的网络字节序数据来得到这个网络字节序地址对应的主机信息,当然这个函数也只有在本地计算的/etc/hosts,/etc/service等文件中查询地址对应的信息,如果在这几个文件中没有那么信息肯定是找不到的
参数:1.addr:注意这个参数是一个void类型,表示这个指针是任意类型的指针,这个addr必须指向一个网络字节序序列,意思就是void必须为in_addr类型的数据
    2.len:为地址长度,在AF_INET网域协议中这个长度一般都填写4
    3.type:地址类型,IPv4类型的地址就填写AF_INET
返回值:1.成功:函数执行成功返回一个hostent类型的指针,这个指针中保存了相关的数据
    2.失败:函数执行失败返回NULL,并且设置errno,用户可以通过WSAGetLastError()函数得到错误信息
    相关错误代码:
        WSANOTINTIALISED(在应用这个API前,必须成功地调用WSAStartup()。)
        WSAHOST_NOT_FOUND(没有找到授权应答主机。)
        WSATRY_AGAIN(没有找到非授权主机,或者SERVERFAIL。)
        WSANO_RECOVERY(无法恢复的错误,FORMERR,REFUSED,NOTIMP。)
        WSANO_DATA(有效的名字,但没有关于请求类型的数据记录。)
        WSAEINPROGRESS(一个阻塞的Windows Sockets操作正在进行。)
        WSAEINTR(阻塞调用被WSACancelBlockingCall()取消了)
注意事项
(1)先介绍一下hostent这个结构体:
    struct
    {
        char* h_name;    //主机名称
        char** h_aliases;    //这个数据是一个二维字符串数组,用来保存主机的别名
        int h_addrtype;    //地址类型
        int h_length;    //地址的长度
        char** h_addr_list;    //主句地址列表,以NULL结尾
    }
(2)struct hostent* gethostbyname(const char* name):函数作用,通过服务名称来得到本地保存的服务信息,和gethostbyaddress()效果是一样的但是参数使用的是name,主机的名字

(8) #include <netdb.h>
    struct servent* getservbyname(const char* name, const char* proto)
函数作用:这个函数通过服务的名称到本地的/etc/hosts,/etc/service文件中查找相关服务的信息,并且将查找到的信息保存再返回的结构体中,这个函数功能和gethostbyaddr()函数的功能类似,但是只是返回的信息不一样
参数:1.name:我们要在本机查找的服务的名称
    2.proto:指向协议的指针,proto指针指向的数据为"tcp","udp"等,如果这个这个参数为空,那么这个函数返回的数据就是从文件中找到的第一个和name匹配的数据
返回值:1.成功:函数执行成功时返回一个servern结构体,这个结构体中保存了我们查找到的相关的信息
    2.失败:函数执行失败的话返回一个NULL,并且设置errno,用户可以通过WSAetLastError()函数来得到相关的错误信息
        WSANOTINITIALISED(在应用这个API前,必须成功地调用WSAStartup()。)
        WSAHOST_NOT_FOUND(没有找到授权应答主机。)
        WSANO_DATA(有效的名字,但没有关于请求类型的数据记录。)
        WSAEINPROGRESS(一个阻塞的Windows Sockets操作正在进行。)
        WSAEINTR(阻塞调用被WSACancelBlockingCall()取消了.)
注意事项
(1)先介绍一下servent这个结构体:
    struct
    {
        char* s_name;    //服务名称
        char** s_aliases;    //服务的别名列表
        int port;    //服务所使用的端口
        char* s_proto;    //服务使用的协议
    }
(2)struct servent* getservbyport(int port, char* proto):这个函数的功能和getservbyname()的功能相似,只不过getservbyname()函数使用的参数是服务的名称,getserbyport函数使用的是端口,返回的都是服务的相关信息

(9) #include <unistd.h>
    int gethostname(char* name, int namelength);
函数作用:这个函数的作用是将当前主机的名字写入到name这个字符串中,最后主机名称以null结尾
参数:1.name:用来保存主机名称的字符串
    2.namelength:这个值指定了name的长度
返回值:1.成功:函数执行成功返回0
    2.失败:函数执行失败时返回-1

(10) #include <sys/socket.h>
   int setsockopt(int socket, int level, int option_name, const void* option_value, size_t option_len)
函数作用:这个函数的作用就是给你建立的套接字设置相关的属性,而且可以在套接字的不同的级别设置相关属性
参数:1.socket:我们要设置属性的相关套接字
    2.level:设置套接字的级别
    3.option_name:设置的选项的名称
    4.option_value:设置的选项的值
    5.option_len:设置的选项的值的大小,单位为字节
************************************************************************************************************************************

参数取值相关介绍

1.level取值:
    (1)SOL_SOCKET(基本套接字)
    (2)IPPROTO_IP(IPv4级别)
    (3)IPPROTO_IPV6(IPv6级别)
    (4)IPPROTO_TCP(TCP协议,底层协议级别)

2.option_name取值:
(1)SOL_SOCKET级别取值:
    SO_BROADCAST      允许发送广播数据    int
    SO_DEBUG                允许调试                    int
    SO_DONTROUTE     不查找路由                int
    SO_ERROR                获得套接字错误        int
    SO_KEEPALIVE         保持连接                    int
   *SO_LINGER               延迟关闭连接            struct linger
    SO_OOBINLINE         带外数据放入正常数据流      int
    SO_RCVBUF              接收缓冲区大小        int
    SO_SNDBUF              发送缓冲区大小        int
    SO_RCVLOWAT         接收缓冲区下限        int
    SO_SNDLOWAT         发送缓冲区下限        int
   *SO_RCVTIMEO          接收超时                    struct timeval(注意这个超时时间其实是等待时间,好比如我们设置超时时间为60s
                                                                               我们运行到recv函数的时候,没有设置这个选项的时候没有数据接
                                                                               收的话程序是会一直阻塞下去的,但是我们设置了超时时间位60s后,
                                                                               程序之后等待60s,超过60s程序套接字就返回)
   *SO_SNDTIMEO          发送超时                    struct timeval
    SO_REUSERADDR   允许重用本地地址和端口        int
    SO_TYPE                     获得套接字类型         int
    SO_BSDCOMPAT      与BSD系统兼容         int
------------------------------------------------------------------------------------------------------------------------------

这里介绍一下SO_LINGER和SO_RCVTIMEO和SO_SNDTIMEO这三个选项的值

(1)SO_LINGER:这个选项设置的是套接字要如何关闭,它使用的值为linger这个结构体
#include <arpa/inet.h>
struct linger
{
    int l_onoff;
    int l_linger;
}
1.当l_onoff = 0时l_linger的值不用设置,这种情况就是当使用close()等函数来关闭套接字的时候,函数会立刻返回,底层会将没有发送完的数据发送完毕之后再释放资源,这个就是优雅的退出

2.当l_onoff != 0时l_linger = 0,这种情况就是当我们调用close()函数来关闭套接字的时候函数会立即返回,但是底层不会将没有发送完毕的数据全部发送出去,而是强制关闭socket的描述符,这个就是强制退出

3.l_onoff != 0时l_linger > 0,这种情况的话,当我们调用close()函数的时候,函数不会立即返回,而是等待一段时间,这个时间的值就是l_linger中保存的值,在这一段时间中底层会将没有发送的数据发送出去,但是如果在l_linger这个时间段中将数据发送完毕了的话,套接字就优雅的对出(和1一样),但是如果在这个时间段中数据没有发送完毕那么就走2路线,既是强制退出

(2)SO_RCVTIMEO和SO_SNDTIMEO这两个选项是用来设置接受和发送的超时时间的,这两个选项使用的值为timeval结构体
struct timeval
{
    time_t tv_sec;    //秒
    long tv_usec;    //微妙
}
------------------------------------------------------------------------------------------------------------------------------
(2)IPPROTO_IP级别取值:
    IP_HDRINCL       在数据包中包含IP首部       int
    IP_OPTINOS        IP首部选项                    int
    IP_TOS                 服务类型
    IP_TTL                  生存时间                        int

(3)IPPRO_TCP级别的取值:
    TCP_MAXSEG              TCP最大数据段的大小       int
    TCP_NODELAY           不使用Nagle算法                int
************************************************************************************************************************************

返回值:1.成功:函数执行成功这个函数返回0
    2.失败:函数执行失败这个函数返回-1,并且设置errno
    错误代码列表:
        EBADF:sock不是有效的文件描述词
        EFAULT:optval指向的内存并非有效的进程空间
        EINVAL:在调用setsockopt()时,optlen无效
        ENOPROTOOPT:指定的协议层不能识别选项
        ENOTSOCK:sock描述的不是套接字

(11) #include <arpa/inet.h>
    int_addr_t inet_addr(const char* ip)
函数作用:这个函数的作用是将一个字符串形式的IP地址转换为网络字节序列形式的int类型的IP地址来使用
************************************************************************************************************************************
相关知识介绍:首先我们的计算机是无法直接使用char类型的IP地址的,在计算机中我们要使用IP地址,也是要将IP地址转换为int类型,而且我们之前说过在计算机中IP和端口的排序方式有网络字节序列和主机字节序列,如果说我们以主机字节序列来使用IP地址的话,可能会因为主句的cpu的架构不同,从而导致A,B两台计算机上原本在程序中写入的地址和端口一样但是转换到计算机中就变为了不同的数据,从而导致A,B两台机器无法通信,从而我们要使用一个统一的序列来保存信息,这个序列就是网络字节序列而inet_addr()函数的作用就是将char类型的ip地址转化为网络字节序列,从而我们就不用使用htonl()函数
************************************************************************************************************************************
参数:ip:我们要进行转换的IP地址(形式为xxx.xxx.xxx)
返回值:1.成功:函数执行成功就返回一个转换为int类型的ip地址
    2.失败:函数执行失败放回NULL

(12) #include <sys/socket.h>
    ssize_t send(int sockfd, const void* buff, size_t nbytes, int flags)
函数作用:向某个建立好连接的套接字发送信息
参数:1.sockfd:建立好连接的套接字的描述符
    2.buff:保存了要发送的数据的结构体
    3.nbytes:实际要发送的数据的字节数
    4.flags:这个参数设置的是一些控制选项
************************************************************************************************************************************

参数取值

1.这里我们只介绍flags的取值:
    (1)MSG_DONTROUTE:绕过路由表查找
    (2)MSG_DONTWAIT:仅本操作非阻塞
    (3)MSG_OOB:发送或接收带外数据
    (4)flags一般取值为0
************************************************************************************************************************************
返回值:1.返回值 = 0:对方关闭了套接字的连接

    2.返回值 > 0:发送出去的数据的字节数
    3.返回值 < 0:数据没有发送成功,错误代码保存在errno中
    错误代码列表:
        EBADF 参数s 非合法的socket处理代码。
        EFAULT 参数中有一指针指向无法存取的内存空间
        ENOTSOCK 参数s为一文件描述词,非socket。
        EINTR 被信号所中断。
        EAGAIN 此操作会令进程阻断,但参数s的socket为不可阻断。
        ENOBUFS 系统的缓冲内存不足
        ENOMEM 核心内存不足
        EINVAL 传给系统调用的参数不正确。
函数知识点介绍
**首先send在发送数据的时候,它会先将要发送的数据的大小和发送缓冲区的总大小进行对比,如果我们要发送的数据的大小nbytes大于发送缓冲区的总大小,那么send函数就会停止发送返回错误,如果发送缓冲区的总大小大于我们要发送的数据的大小,那么send就会看看发送缓冲区中是否在发送数据,如果是在发送数据那么send函数就等待数据发送完毕,如果缓冲区中没有数据再发送那么send函数就会对比我们要发送的数据和缓冲区剩余的大小,如果发送的数据的小大于缓冲区剩余的大小,send就会等待缓冲区中的数据发送完毕,如果小于那么send就会直接将要发送的数据复制到发送缓冲区
    注意send函数是不会发送的数据,send函数只是将要发送的数据复制到发送缓冲区中,发送的操作函数由底层协议来完成,send执行成功之后返回复制到缓冲区中的数据的大小

**这个send()函数和write()很相似,有很多套接字编程也是直接使用的write(),但是write()是一个系统函数,write()的作用是将数据写到一个文件描述符中,所以这个函数并只不针对于套接字,任何文件描述符都可以使用他向其指向的文件中写入数据,我们之前说过在Linux的程序中我们操作的套接字也是一文件描述符存在的,所以我们使用write()来向socket的文件描述符中吸入数据来达到传输数据也是可行的,但是要知道write()函数是一个系统调用函数,系统调用函数在执行的时候Linux会先进入到内核模式去执行它,所以write()的执行比send()更加浪费时间,而且send()比write()可控制的选项更多,也更加安全所以大家在编程的时候尽量使用send()

**注意我们首先注意一点,我们看一串代码:
    char ss[1024];
    memset(ss, 0x00, sizeof(ss));
    //给ss附加数据
    send(sockfd, ss, sizeof(ss)) / send(sockfd, ss, strlen())
这里有一个疑问我们是使用send(sockfd, ss, sizeof(ss))还是使用send(sockfd, ss, strlen()),注意send()函数是有多少送多少首先sizeof(ss)不等于strlen(ss),sizeof是整个字符串的字节总数,使用memset()会将ss这个字符串使用0x00填满,所以sizeof(ss)=1024,而strlen(ss)表示的是字符串的数据长度,如果ss总保存的数据为"12",那么strlen(ss) = 2,所以当我们使用send()函数发送数据的时候就只会发送12,但是我们使用sizeof(),send函数会发送1024个字节的数据,出了ss中的12,还会加上一大串的"\0"(0x00)

(13) #include <sys/socket.h>
    int recv(int sockfd, const void* buffer, int len, int flags)
函数作用:这个函数的作用就是将套接字中保存的数据取出来,保存到buffer中
参数:1.socket:我们要取出数据的套接字
    2.buffer:保存从套接字中取出来的数据的字符串
    3.len:要取出的数据的字节数
    4.flag:这个参数设置的是一些控制选项
************************************************************************************************************************************
参数取值
1.这里我们只介绍flags的取值:
    (1)MSG_DONTWAIT    仅本操作非阻塞
    (2)MSG_OOB         发送或接收带外数据
    (3)MSG_PEEK        窥看外来消息
    (4)MSG_WAITALL     等待所有数据
    (5)flag一般设置为0
************************************************************************************************************************************
返回值:1.返回值 < 0:执行出错设置errno为某个指
    2返回值 = 0:对方关闭了套接字的连接
    3.返回值 > 0:接收到的数据的数据的大小
    错误代码列表:
        EAGAIN:套接字已标记为非阻塞,而接收操作被阻塞或者接收超时
        EBADF:sock不是有效的描述词
        ECONNREFUSE:远程主机阻绝网络连接
        EFAULT:内存空间访问出错
        EINTR:操作被信号中断
        EINVAL:参数无效
        ENOMEM:内存不足
        ENOTCONN:与面向连接关联的套接字尚未被连接上
        ENOTSOCK:sock索引的不是套接字
函数知识点介绍
**首先在调用recv函数接收数据的时候,recv函数会先将查看sockfd套接字的底层协议是否还在接收数据,如果还在接收数据的话,那么这个recv函数就会先等待,等sockfd的底层连接协议将数据传输完毕之后,recv函数就会将sockfd指向的接收缓冲区中的数据复制出来,注意recv进行的操作并不是接收而是复制,如果说数据一次没有办法复制完,那就需要多次调用recv函数,如果在底层协议传输数据的时候出现了网络错误,那么recv函数就直接返回错误,如果在复制的时候也出现了错误也返回错误,如果在底层协议传输数据的时候出现了网络中断,recv就直接返回0

**在我们看套接字程序的时候经常可以看到有很多程序时候的是read()函数,read()函数的和write()函数一样,也是一个系统调用函数,同样,当我们的Linux系统在执行这个函数的时候写会切换到内核模式,再去运行这个函数,相对的就要花很多的时间,而且read()函数并不是针对于套接字来设计的,他可以从任意的一个文件描述符中读取那个文件描述符指向的文件的数据,所以相对而言我们还是使用recv()函数比较好,recv()比起read()而言,可操作性十分广,而且安全

5.相关代码

注意一下代码只能适用于单个套接字的连接收发,而且一下套接字使用的模式是阻塞的模式

(1)服务端:

/***************************************************************************
 *文件名称:socket_server.c
 * 
 *文件作用:这个文件的作用是用来编写一个套接字的服务端,这个服务端用来接收
 *客户端发送过来的信息,并且返回相关信息
 * 
 ****************************************************************************/



#include <stdio.h>            //引入stdio.h头文件,这个头文件中包含了IO操作相关的函数
#include <stdlib.h>           //引入stdlib.h头文件,这个头文件中包含了一些类型和相关的通用函数
#include <string.h>           //引入string.h头文件,这个头文件中包含了字符串操作的相关函数
#include <errno.h>            //引入errno.h头文件,这个头文件中包含了错误处理的相关函数
#include <sys/socket.h>       //引入sys/目录下面的socket.h头文件,这个头文件中包含了套接字操作的相关函数
#include <sys/types.h>        //引入sys/目录下面的types.h头文件,这个头文件中包含了一些类型
#include <netinet/in.h>       //引入netinet/目录下面的in.h头文件,这个头文件中中包含了套接字地址结构体和本地地址和网络字节序列相互转换的相关的函数

int main(int argc, char* argv[])
{
    //定义要使用的相关边来------------------------------
    int ret;                                    //定义一个int类型的变量,来保存函数运行的结果
    int len;                                    //定义一个int类型的变量,来保存我们操作的字符串的相关数据
    int socket_server;                          //定义一个int类型的变量,这个变量用来保存服务端的套接字
    int socket_connect;                         //定义一个int类型的变量,这个变量用来保存客户端和服务端连接好了的套接字
    struct sockaddr_in server_address;          //定义一个sockaddr_in结构体,这个结构体用来保存服务端的地址相关的参数
    struct sockaddr_in client_address;          //定义一个sockaddr_in结构体,这个结构体用来保存接收到的客户端的地址相关的参数
    struct linger socket_exit;                  //定义一个linger结构体,这个结构体来确定套接子的退出的方式
    struct timeval socket_OT;                   //定义一个timeval结构体,这个结构体用来定义套接字的超时的时间
    char RecvBuffer[1024];                      //定义一个char类型的字符串,来保存函数函数接收到的数据
    char SendBuffer[1024];                      //定义一个char类型的字符串,来保存我们要发送的数据
    char* ptr;                                  //定义一个char*类型的指针用来转换字符串
	
    //初始化我们要使用的相关数据-----------------------
    memset(&server_address, 0x00, sizeof(server_address));
    memset(&client_address, 0x00, sizeof(client_address));
    memset(RecvBuffer, 0x00, sizeof(RecvBuffer));
    memset(SendBuffer, 0x00, sizeof(SendBuffer));
	
    //开始创建服务端套接字------------------------------
    socket_server = socket(AF_INET, SOCK_STREAM, 0);        //使用socket函数来创建一个套接字,这个套接字域为AF_INET,类型为流套接字
    if(socket_server < 0)                                   //如果socket_server < 0表示我们创建套接字失败
    {
        fprintf(stderr, "Create socket error, the error is %s\n", strerror(errno));    //输出错误的信息
        exit(EXIT_FAILURE);                                 //程序异常退出
    }
	
    server_address.sin_family = AF_INET;                    //为服务断套接字的地址设置属性,这个地址是AF_INET类型的地址
    server_address.sin_addr.s_addr = htonl(INADDR_ANY);     //INADDR_ANY表示任意地址,指我们这个服务端可以接收任意地址的连接,并且使用htonl()将地址转换为网络字节序列
    server_address.sin_port = htons(12580);                 //设置我们服务端的监听的端口为12580,而且这个端口我们转换为网络字节序列
    ret = bind(socket_server, (struct sockaddr*)&server_address, sizeof(server_address));        //使用bind()函数将套接字和地址结构体绑定,从而为我们创建的套接字命名
    if(ret < 0)                                             //如果ret < 0表示我们的命名失败了
    {
        fprintf(stderr, "Bind error, The error is %s\n", strerror(errno));        //输出错误信息
        exit(EXIT_FAILURE);                                 //进程异常退出
    }
	
    //为套接字设置属性----------------------------------- 
    socket_exit.l_onoff = 0;                                //设置linger结构体中的l_onoff选项为0表示套接字要发送完数据再退出
    socket_OT.tv_sec = 60;                                  //设置timeval结构体中的tv_sec这个变量为60s
    socket_OT.tv_usec = 0;                                  //设置timeval结构体中的tv_usec这个变量为0us
	
    setsockopt(socket_server, SOL_SOCKET, SO_LINGER, &socket_exit, sizeof(socket_exit));    //设置套接字的退出方式
    setsockopt(socket_server, SOL_SOCKET, SO_RCVTIMEO, &socket_OT, sizeof(socket_OT));      //设置套接字的接收超时
    setsockopt(socket_server, SOL_SOCKET, SO_SNDTIMEO, &socket_OT, sizeof(socket_OT));      //设置套接字的发送超时
	
    //开始监听套接字,等待连接-------------------------
    ret = listen(socket_server, 1);                         //使用listen()函数来接受
    if(ret < 0)
    {
        fprintf(stderr, "listen error, The error is %s\n", strerror(errno));                //输出错误信息
        exit(EXIT_FAILURE);                                 //程序异常退出
    }
	
    //等待连接和操作套接字---------------------------
    int size = sizeof(client_address);                      //得到结构体的大小
    socket_connect = accept(socket_server, (struct sockaddr*)&client_address, &size);       //使用accpet()函数接受客户端的连接,&sizeof(XX)会报出单目运算符错误
    if(socket_connect < 0)                                  //socket_client < 0表示接受套接字失败
    {
        fprintf(stderr, "accpet error, The error is %s\n", strerror(errno));                //输出错误信息 
        exit(EXIT_FAILURE);                                 //进程异常退出
    }
    printf("accpet success!\n");
		
    //开始不断的接受数据---------------------------------
    while(1)
    {	
        len = recv(socket_connect, RecvBuffer, sizeof(RecvBuffer), 0);                      //使用recv()函数来接受客户端发送过来的数据
        if(len == 0)
        {
            printf("Connect Over--1!\n");                   //len为0的话,表示连接中断
            exit(EXIT_FAILURE);                             //进程异常退出
        }
        else if(len < 0)                                    //len < 0的话,表示连接失败
        {
            fprintf(stderr, "Recv error, the error is %s\n", strerror(errno));              //输出错误信息
            exit(EXIT_FAILURE);                             //进程错误退出
        }
        printf("We recv %d bytes data, The data we recv is %s\n", len, RecvBuffer);         //输出接收到的数据
		
        for(ptr = RecvBuffer; *ptr; ptr++)                  //将字符串中的数据转化为大写
        {
            *ptr = toupper(*ptr);										
        }
		
        strcpy(SendBuffer, RecvBuffer);                     //将变为大写的数据复制到SendBuffer中
        len = send(socket_connect, SendBuffer, strlen(SendBuffer), 0);                      //使用send()函数将处理完成的数据发送出去
        if(len == 0)
        {
            printf("Connect Over!--2\n");
            exit(EXIT_FAILURE);                             //进程异常退出
        }
        else if(len < 0)
        {
            fprintf(stderr, "Send error, the error is %s\n", strerror(errno));              //输出错误信息
            exit(EXIT_FAILURE);                             //程序异常退出
        }
        printf("We send %d bytes data, The data we send is %s\n", len, SendBuffer);         //输出信息
        printf("-----------------------------------------------------------------------\n");
        memset(RecvBuffer, 0x00, sizeof(RecvBuffer));       //重新清空RecvBuffer中的数据,一让下次接受没有上次接受的数据
    }
	
    close(socket_connect);                                  //关闭连接好的套接字
    printf("server will exit!\n");                          //输出信息
    exit(EXIT_SUCCESS);                                     //进程成功退出
}

(2)客户端:

/***************************************************************************
 *文件名称:socket_client.c
 * 
 *文件作用:这个文件的作用是用来编写一个套接字的客户端,这个客户端用来向
 *服务端发送信息,并且接受服务端返回过来的信息
 * 
 ****************************************************************************/
 
#include <stdio.h>                    //引入stdio.h头文件,这个头文件中保存了Linux的相关的IO函数
#include <stdlib.h>                   //引用stdlib.h头文件,这个头文件中保存了5种类型和一些相通用函数
#include <string.h>                   //引入string.h头文件,这个头文件中保存了字符串操作的相关函数
#include <errno.h>                    //引入errno.h头文件,这个头文件中包含了错误操作相关的函数
#include <netinet/in.h>               //引入netinet/目录下面的in.h头文件,这个头文件中保存了套接字地址的相关结构体和网络字节序列转换的相关函数
#include <arpa/inet.h>				  //引入arpa/目录下面的inte.h头文件,这个头文件中保存了IP地址转换为网络字节序列的相关函数
#include <sys/types.h>                //引入sys/目录下面的types.h头文件,这个头文件中保存了一些基本的数据类型
#include <sys/socket.h>               //引入sys/目录下面的socket.h头文件,这个头文件中保存了套接子建立的相关函数

int main(int argc, char* argv[])
{
    //定义要使用的相关变量------------------------------
    int ret;                          //定义一个int类型的变量,来保存函数运行的结果
    int len;                          //定义一个int类型的变量,来记录我们操作的数据的长度
    int client_socket;                //定义一个int类型变量,来保存客户端建立起来的套接字
    char IP[20];                      //定义一个字符串来保存我们要连接的IP地址
    char RecvBuffer[1024];            //定义一个字符串来保存我们要接收的数据
    char SendBuffer[1024];            //定义一个字符串来保存我们要发送的数据
    struct sockaddr_in server_addr;   //定义一个sockaddr_in结构体,这个结构体的作用是用来保存客户端要连接的服务端套接子的相关地址信息
    struct linger socket_exit;        //定义一个linger结构体,这个结构体保存了套接字退出的方式
    struct timeval socket_OT;         //定义一个timeval结构体,这个结构体用来保存套接字发送超时的时间
	
    //对相关变量进行初始化------------------------------
    memset(IP, 0x00, sizeof(IP));                        //初始化保存IP的字符串数组
    memset(&socket_exit, 0x00, sizeof(socket_exit));     //初始化结构体
    memset(&socket_OT, 0x00, sizeof(socket_OT));         //初始化结构体
    memset(&server_addr, 0x00, sizeof(server_addr));     //对结构体先进行初始化
    memset(SendBuffer, 0x00, sizeof(SendBuffer));        //对保存发送数据的字符串进行初始化
    memset(RecvBuffer, 0x00, sizeof(RecvBuffer));        //对保存接收数据的数组进行初始化
	
    //开始创建套接字--------------------------------------
    if(argc != 2)                                        //参数不为2,表示我们要是连接本地地址
    {
        strcpy(IP, "127.0.0.1");                         //将本地地址保存到IP中
    }
    else
    {
        strcpy(IP, argv[1]);                             //如果参数为2,表示我们连接的IP地址为传给函数的一个参数
    }
	
    client_socket = socket(AF_INET, SOCK_STREAM, 0);     //使用socket()函数来建立一个未命名的套接字,这个套接字为IPv4网域的,类型为流套接字,协议使用默认协议
    if(client_socket < 0)                                //生成的套接字小于0表示socket()函数运行失败
    {
        fprintf(stderr, "socket create error! The error is %s\n", strerror(errno));        //向标准错误输出信息,套接字创建失败,并且输出错误信息
        exit(EXIT_FAILURE);                              //程序失败退出
    }
	
    server_addr.sin_family = AF_INET;                    //服务端地址协议族为AF_INET
    server_addr.sin_addr.s_addr = inet_addr(IP);         //服务端使用inet_addr()函数,将IP地址变为网络字节序列
    server_addr.sin_port = htons(12580);                 //服务端的监听的端口为12580将端口12580转化为网络字节序列
	
    //为套接字设置属性-----------------------------------
    socket_exit.l_onoff = 0;                             //设置linger结构体中的l_onoff选项为0表示套接字要发送完数据再退出
    socket_OT.tv_sec = 60;                               //设置timeval结构体中的tv_sec这个变量为60s
    socket_OT.tv_usec = 0;                               //设置timeval结构体中的tv_usec这个变量为0us
	
    setsockopt(client_socket, SOL_SOCKET, SO_LINGER, &socket_exit, sizeof(socket_exit));    //设置套接字的退出方式
    setsockopt(client_socket, SOL_SOCKET, SO_RCVTIMEO, &socket_OT, sizeof(socket_OT));      //设置套接字的接受超时时间
    setsockopt(client_socket, SOL_SOCKET, SO_SNDTIMEO, &socket_OT, sizeof(socket_OT));      //设置套接字的发送超时时间
	
    //使用我们建立好的套接字来连接远程服务端--------
    ret = connect(client_socket, (struct sockaddr*)&server_addr, sizeof(server_addr));      //使用我们建立的套接字来连接客户端,如果没有连接成功他会阻塞一段时间
    if(ret < 0)
    {
        fprintf(stderr, "connect Error! The error is %s\n", strerror(errno));               //输出连接失败,并且输出错误信息
        exit(EXIT_FAILURE);                              //程序异常退出
    }
    printf("Connect to Server success!\n");              //如果程序没有异常退出,输出信息,连接到服务器成功
	
    //使用无限循环来发送和接受信息--------------------
    while(1)
    {
        //开始发送数据-------------------------------------
        printf("Please input the data you want send to the server!\n");
        printf("Input \"quit\" to quit!\n");
        fgets(SendBuffer, sizeof(SendBuffer), stdin);    //将我们要发送的数据写入到SendBuffer中,scanf()是要忽略空格的,所以这里使用的为fgets
		
        printf("The data we input is %s\n", SendBuffer);
		
        if(strncmp(SendBuffer, "quit", 4) == 0)          //如果输入的信息为q表示我们要退出客户端程序
        {
            printf("The client exit!\n");
            exit(EXIT_SUCCESS);                          //程序正常退出
        }
		
        len = send(client_socket, SendBuffer, strlen(SendBuffer), 0);         //使用send()函数来发出信息,注意这里使用的是strlen(SendBuffer)而不是sizeof(SendBuffer)
        if(len == 0)
        {
            printf("Connect Over!\n");
            exit(EXIT_FAILURE);                                               //进程异常退出
        }
        else if(len < 0)
        {
            fprintf(stderr, "Send error, The error is %s\n", strerror(errno));//输出错误信息
            exit(EXIT_FAILURE);                                               //程序异常终止
        }
        printf("We Send %d bytes data, The data we send is %s\n", len, SendBuffer);	//输出我们发送的数据的字节数,和我们发送的数据的实体
		
        //接受从服务端传回来的数据----------------------
        len = recv(client_socket, RecvBuffer, sizeof(RecvBuffer), 0);               //将服务器返回的数据保存到RecvBuffer中
        if(len == 0)
        {
            printf("Connect Over!\n");
            exit(EXIT_FAILURE);                          //进程异常退出
		}
        else if(len < 0)
        {
            fprintf(stderr, "Recv error, The error is %s\n", strerror(errno));      //输出错误信息
            exit(EXIT_FAILURE);                          //程序异常终止
        }
        printf("We Recv %d bytes data, The data we recv is %s\n", len, RecvBuffer);
        //close(client_socket);                                                     //关闭连接好的套接字,从而方便下一次使用,不能使用一个连接来重复发送数据,一个连接发送一次数据之后就不能再次使用了
        printf("-----------------------------------------------------------------------\n");
    }

    close(client_socket);                                //关闭连接好的套接字,从而方便下一次使用,不能使用一个连接来重复发送数据,一个连接发送一次数据之后就不能再次使用了
    printf("The socket is close! Programe exit!\n");     //输出信息
    exit(EXIT_SUCCESS);                                  //进程成功退出
}


  • 4
    点赞
  • 1
    评论
  • 6
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值