linux网络编程 - Go语言中文社区

linux网络编程


linux网络编程


1.网络编程相关协议

1.1. TCP/IP协议概述

协议protocol:通信双方必须遵循的规矩 由iso规定 rpc文档
osi参考模型:(应-表-会-传-网-数-物)
应用层 表示层 会话层 传输层 网络层 数据链路层 物理层
tcp/ip模型4层:

应用层:http超文本传输协议 ftp文件传输协议 telnet远程登录ssh安全外壳协议smtp简单邮件发送 pop3收邮件
传输层:tcp传输控制协议,udp用户数据包协议
网络层:ip网际互联协议 icmp网络控制消息协议 igmp网络组管理协议
网络接口层:arp地址转换协议,rarp反向地址转换协议,mpls多协议标签交换

  • TCP协议:传输控制协议 面向连接的协议 能保证传输安全可靠 速度慢(有3次握手)
  • UDP协议:用户数据包协议 非面向连接 速度快 不可靠
    通常是ip地址后面跟上端口号:ip用来定位主机 port区别应用(进程)

http的端口号80 ssh–>22 telnet–>23 ftp–>21 用户自己定义的通常要大于1024

1.2. OSI参考模型及TCP/IP参考模型

OSI参考模型和TCP/IP参考模型对应关系 and TCP/IP各层之间的协议
TCP/IP协议族的每一层的作用:

网络接口层:负责将二进制流转换为数据帧,并进行数据帧的发送和接收。要注意的是数据帧是独立的网络信息传输单元。
网络层:负责将数据帧封装成IP数据报,并运行必要的路由算法。
传输层:负责端对端之间的通信会话连接和建立。传输协议的选择根据数据传输方式而定。
应用层:负责应用程序的网络访问,这里通过端口号来识别各个不同的进程。

TCP/IP协议族的每一层协议的相关注解:

ARP:(地址转换协议)用于获得同一物理网络中的硬件主机地址。
MPLS:(多协议标签交换)很有发展前景的下一代网络协议。
IP:(网际互联协议)负责在主机和网络之间寻址和路由数据包。
ICMP:(网络控制消息协议)用于发送报告有关数据包的传送错误的协议。
IGMP:(网络组管理协议)被IP主机用来向本地多路广播路由器报告主机组成员的协议。
TCP:(传输控制协议)为应用程序提供可靠的通信连接。适合于一次传输大批数据的情况。并适用于要求得到相应的应用程序。
UDP:(用户数据包协议)提供了无连接通信,且不对传送包进行可靠的保证。适合于一次传输少量数据。
数据在各层添加te'd标签的流程图

1.3. TCP协议

(1)概述
TCP是TCP/IP体系中面向连接的运输层协议,它提供全双工可靠交付的服务。它采用许多机制来确保端到端结点之间的可靠数据传输,如采用序列号、确认重传、滑动窗口等。
- 序列号:TCP要为所发送的每一个报文段加上序列号,保证每一个报文段能被接收方接收,并只被正确的接收一次。
- 确认重传:TCP采用具有重传功能的积极确认技术作为可靠数据流传输服务的基础。这里“确认”是指接收端在正确收到报文段之后向发送端回送一个确认(ACK)信息。发送方将每个已发送的报文段备份在自己的缓冲区里,而且在收到相应的确认之前是不会丢弃所保存的报文段的。“积极”是指发送发在每一个报文段发送完毕的同时启动一个定时器,加入定时器的定时期满而关于报文段的确认信息还没有达到,则发送发认为该报文段已经丢失并主动重发。为了避免由于网络延时引起迟到的确认和重复的确认,TCP规定在确认信息中捎带一个报文段的序号,使接收方能正确的将报文段与确认联系起来。
- 滑动窗口:采用可变长的滑动窗口协议进行流量控制,以防止由于发送端与接收端之间的不匹配而引起的数据丢失。这里所采用的滑动窗口协议与数据链路层的滑动窗口协议在工作原理上完全相同,唯一的区别在于滑动窗口协议用于传输层是为了在端对端节点之间实现流量控制,而用于数据链路层是为了在相邻节点之间实现流量控制。TCP采用可变长的滑动窗口,使得发送端与接收端可根据自己的CPU和数据缓存资源对数据发送和接收能力来进行动态调整,从而灵活性更强,也更合理。

参见博文tcp窗口滑动以及拥塞控制

(2)三次握手
在利用TCP实现源主机和目的主机通信时,目的主机必须同意,否则TCP连接无法建立。为了确保TCP连接的成功建立,TCP采用了一种称为三次握手的方式,三次握手方式使得“序号/确认号”系统能够正常工作,从而使它们的序号达成同步。如果三次握手成功,则连接建立成功,可以开始传送数据信息。
其三次握手分别为:

1.源主机A的TCP向主机B发送连接请求报文段,其首部中的SYN(同步)标志位应置为1,表示想跟目标主机B建立连接,进行通信,并发送一个同步序列号seq为x(例:x=100)进行同步,表明在后面传送数据时的第一个数据字节的序号为x+1(即101)。
2.目标主机B的TCP收到连接请求报文段后,如同意,则发回确认。再确认报中应将ACK位和SYN位置为1.确认号ack置为x+1,同时也为自己选择一个序号seq为y。
3.源主机A的TCP收到目标主机B的确认后要想目标主机B给出确认。其ACK置为1,确认号为y+1,而自己的序号为x+1。TCP的标准规定,SYN置1的报文段要消耗掉一个序号。

运行客户进程的源主机A的TCP通知上层应用进程,连接已经建立。当源主机A向目标主机B发送第一个数据报文段时,其序号仍为x+1,因为前一个确认报文段并不消耗序号。
当运行服务进程的目标主机B的TCP收到源主机A的确认后,也通知其上层应用进程,连接已经建立。至此建立了一个全双工的连接。

TCP三次握手过程

三次握手:为应用程序提供可靠的通信连接。适合于一次传输大批数据的情况。并适用于要求得到响应的应用程序。
(3)四次挥手
当客户A 没有东西要发送时就要释放 A 这边的连接,A会发送一个报文(没有数据),其中 FIN 设置为1, 服务器B收到后会给应用程序一个信息,这时A那边的连接已经关闭,即A不再发送信息(但仍可接收信息)。 A收到B的确认后进入等待状态,等待B请求释放连接, B数据发送完成后就向A请求连接释放,也是用FIN=1 表示, 并且用 ack = u+1(如图), A收到后回复一个确认信息,并进入 TIME_WAIT 状态, 等待 2MSL 时间。

TCP四次挥手

为什么要等待呢?

为了这种情况: B向A发送 FIN = 1 的释放连接请求,但这个报文丢失了, A没有接到不会发送确认信息, B 超时会重传,这时A在 WAIT_TIME 还能够接收到这个请求,这时再回复一个确认就行了。(A收到 FIN = 1 的请求后 WAIT_TIME会重新记时)

另外服务器B存在一个保活状态,即如果A突然故障死机了,那B那边的连接资源什么时候能释放呢? 就是保活时间到了后,B会发送探测信息, 以决定是否释放连接。

(4)TCP数据报头
TCP头信息

这里写图片描述

  • 源端口、目的端口:16位长。标识出远端和本地的端口号。
  • 序号:32位长,标识发送的数据报的顺序。TCP是面向字节流的,要发送的字节流的起始序号是连接建立之初对每一个字节都顺序编号获得的。TCP数据报头中的序号指本段报文所发送的第一个字节的序号
  • 确认号:32位长。希望收到的下一个数据报的序列号。
  • TCP头长:4位长。表明TCP头中包含多少个32位字。
  • 6位未用。
  • ACK:ACK位置1表明确认号是合法的。如果ACK为0,那么数据报不包含确认信息,确认字段被省略。
  • PSH:表示是带有PUSH标志的数据。接收方因此请求数据报一到便可送往应用程序而不必等到缓冲区装满时才发送。
  • RST:用于复位由于主机崩溃或其他原因而出现的错误的连接。还可以用于拒绝非法的数据报或拒绝连接请求。
  • SYN:用于建立连接。
  • FIN:用于释放连接。
  • 窗口大小:16位长。窗口大小字段表示在确认了字节之后还可以发送多少个字节。
  • 校验和:16位长。是为了确保高可靠性而设置的。它校验头部、数据和伪TCP头部之和。
  • 可选项:0个或多个32位字。包括最大TCP载荷,窗口比例、选择重复数据报等选项。

1.4. UDP协议

(1)概述
UDP即用户数据报协议,它是一种无连接协议,因此不需要像TCP那样通过三次握手来建立一个连接。同时,一个UDP应用可同时作为应用的客户或服务器方。由于UDP协议并不需要建立一个明确的连接,因此建立UDP应用要比建立TCP应用简单得多。
它比TCP协议更为高效,也能更好地解决实时性的问题。如今,包括网络视频会议系统在内的众多的客户/服务器模式的网络应用都使用UDP协议。
(2)Udp数据包头格式
这里写图片描述

1.5. 协议的选择

(1)对数据可靠性的要求
对数据要求高可靠性的应用需选择TCP协议,如验证、密码字段的传送都是不允许出错的,而对数据的可靠性要求不那么高的应用可选择UDP传送。
(2)应用的实时性
TCP协议在传送过程中要使用三次握手、重传确认等手段来保证数据传输的可靠性。使用TCP协议会有较大的时延,因此不适合对实时性要求较高的应用,如VOIP、视频监控等。相反,UDP协议则在这些应用中能发挥很好的作用。
(3)网络的可靠性
由于TCP协议的提出主要是解决网络的可靠性问题,它通过各种机制来减少错误发生的概率。因此,在网络状况不是很好的情况下需选用TCP协议(如在广域网等情况),但是若在网络状况很好的情况下(如局域网等)就不需要再采用TCP协议,而建议选择UDP协议来减少网络负荷。

2. 网络相关概念

1)套接口的概念:
套接口,也叫“套接字”。是操作系统内核中的一个数据结构,它是网络中的节点进行相互通信的门户。它是网络进程的ID。网络通信,归根到底还是进程间的通信(不同计算机上的进程间通信)。在网络中,每一个节点(计算机或路由)都有一个网络地址,也就是IP地址。两个进程通信时,首先要确定各自所在的网络节点的网络地址。但是,网络地址只能确定进程所在的计算机,而一台计算机上很可能同时运行着多个进程,所以仅凭网络地址还不能确定到底是和网络中的哪一个进程进行通信,因此套接口中还需要包括其他的信息,也就是端口号(PORT)。在一台计算机中,一个端口号一次只能分配给一个进程,也就是说,在一台计算机中,端口号和进程之间是一一对应关系。所以,使用端口号和网络地址的组合可以唯一的确定整个网络中的一个网络进程。
例如,如网络中某一台计算机的IP为10.92.20.160,操作系统分配给计算机中某一应用程序进程的端口号为1500,则此时 10.92.20.160 1500就构成了一个套接口。
2)端口号的概念:
在网络技术中,端口大致有两种意思:一是物理意义上的端口,如集线器、交换机、路由器等用于连接其他网络设备的接口。二是指TCP/IP协议中的端口,端口号的范围从0~65535,一类是由互联网指派名字和号码公司ICANN负责分配给一些常用的应用程序固定使用的“周知的端口”,其值一般为0~1023.例如http的端口号是80,ftp为21,ssh为22,telnet为23等。还有一类是用户自己定义的,通常是大于1024的整型值。
3)ip地址的表示:
通常用户在表达IP地址时采用的是点分十进制表示的数值(或者是为冒号分开的十进制Ipv6地址),而在通常使用的socket编程中使用的则是二进制值,这就需要将这两个数值进行转换。
ipv4地址:32bit, 4字节,通常采用点分十进制记法。
例如对于:10000000 00001011 00000011 00011111
点分十进制表示为:128.11.3.31

ip地址的分类:
这里写图片描述
特殊的ip地址:

这里写图片描述

2.1. socket概念

Linux中的网络编程是通过socket接口来进行的。socket是一种特殊的I/O接口,它也是一种文件描述符。它是一种常用的进程之间通信机制,通过它不仅能实现本地机器上的进程之间的通信,而且通过网络能够在不同机器上的进程之间进行通信。
每一个socket都用一个半相关描述{协议、本地地址、本地端口}来表示;一个完整的套接字则用一个相关描述{协议、本地地址、本地端口、远程地址、远程端口}来表示。socket也有一个类似于打开文件的函数调用,该函数返回一个整型的socket描述符,随后的连接建立、数据传输等操作都是通过socket来实现的;

2.2. socket类型

  • 流式socket(SOCK_STREAM) 用于TCP通信
    流式套接字提供可靠的、面向连接的通信流;它使用TCP协议,从而保证了数据传输的正确性和顺序性。
  • 数据报socket(SOCK_DGRAM) 用于UDP通信
    数据报套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证是可靠、无差错的。它使用数据报协议UDP。
  • 原始socket(SOCK_RAW) 用于新的网络协议实现的测试等
    原始套接字允许对底层协议如IP或ICMP进行直接访问,它功能强大但使用较为不便,主要用于一些协议的开发。

2.3. socket信息数据结构

struct sockaddr 
{
    unsigned short sa_family;   /*地址族*/
    char sa_data[14];           /*14字节的协议地址,包含该socket的IP地址和端口号。*/
};
struct sockaddr_in 
{
    short int sin_family;       /*地址族*/
    unsigned short int sin_port;    /*端口号*/
    struct in_addr sin_addr;        /*IP地址*/
    unsigned char sin_zero[8];  /*填充0 以保持与struct sockaddr同样大小*/
};

参数sin_port和sin_addr都是网络字节序

struct in_addr
{
    unsigned long int  s_addr;  // 32位IPv4地址,网络字节序
};

参数s_addr保存的是网络字节序
头文件

2.4. 数据存储优先顺序的转换

计算机数据存储有两种字节优先顺序:高位字节优先(称为大端模式)和低位字节优先(称为小端模式)。内存的低地址存储数据的低字节,高地址存储数据的高字节的方式叫小端模式。内存的高地址存储数据的低字节,低地址存储数据高字节的方式称为大端模式。

eg:对于内存中存放的数0x12345678来说
如果是采用大端模式存放的,则其真实的数是:0x12345678
如果是采用小端模式存放的,则其真实的数是:0x78563412

如果称某个系统所采用的字节序为主机字节序,则它可能是小端模式的,也可能是大端模式的。而端口号和IP地址都是以网络字节序存储的,不是主机字节序,网络字节序都是大端模式。要把主机字节序和网络字节序相互对应起来,需要对这两个字节存储优先顺序进行相互转化。这里用到四个函数:htons(),ntohs(),htonl()和ntohl().这四个地址分别实现网络字节序和主机字节序的转化,这里的h代表host,n代表network,s代表short,l代表long。通常16位的IP端口号用s代表,而IP地址用l来代表。
函数原型如下:
这里写图片描述

图片出错,错误为:uint32_t ntohs(unit32_t net32bit)
改为:uint32_t ntohl(unit32_t net32bit);

2.5. 地址格式转化

通常用户在表达地址时采用的是点分十进制表示的数值(或者是为冒号分开的十进制Ipv6地址),而在通常使用的socket编程中使用的则是32位的网络字节序的二进制值,这就需要将这两个数值进行转换。这里在Ipv4中用到的函数有inet_aton()、inet_addr()和inet_ntoa(),而IPV4和Ipv6兼容的函数有inet_pton()和inet_ntop()。
IPv4的函数原型:

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char *straddr, struct in_addr *addrptr);
char *inet_ntoa(struct in_addr inaddr);
in_addr_t inet_addr(const char *straddr);

1.函数inet_aton():将点分十进制数的IP地址转换成为网络字节序的32位二进制数值。返回值:成功,则返回1,不成功返回0.
- 参数straddr:存放输入的点分十进制数IP地址字符串。
- 参数addrptr:传出参数,保存网络字节序的32位二进制数值。

2.函数inet_ntoa():将网络字节序的32位二进制数值转换为点分十进制的IP地址。
3.函数inet_addr():功能与inet_aton相同,但是结果传递的方式不同。inet_addr()若成功则返回32位二进制的网络字节序地址。

IPv4和IPv6的函数原型:

#include <arpa/inet.h>
int inet_pton(int family, const char *src, void *dst);
const char *inet_ntop(int family, const void *src, char *dst, socklen_t len);

函数inet_pton跟inet_aton实现的功能类似,只是多了family参数,该参数指定为AF_INET,表示是IPv4协议,如果是AF_INET6,表示IPv6协议。
函数inet_ntop跟inet_ntoa类似,其中len表示表示转换之后的长度(字符串的长度)。
Example:

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
    char ip[] = "192.168.0.101";

    struct in_addr myaddr;
    /* inet_aton */
    int iRet = inet_aton(ip, &myaddr);
    printf("%xn", myaddr.s_addr);

    /* inet_addr */
    printf("%xn", inet_addr(ip));

    /* inet_pton */
    iRet = inet_pton(AF_INET, ip, &myaddr);
    printf("%xn", myaddr.s_addr);

    myaddr.s_addr = 0xac100ac4;
    /* inet_ntoa */
    printf("%sn", inet_ntoa(myaddr));

    /* inet_ntop */
    inet_ntop(AF_INET, &myaddr, ip, 16);
    puts(ip);
    return 0;
}

2.6. 名字地址转化

通常,人们在使用过程中都不愿意记忆冗长的IP地址,尤其到Ipv6时,地址长度多达128位,那时就更加不可能一次性记忆那么长的IP地址了。因此,使用主机名或域名将会是很好的选择。主机名与域名的区别:主机名通常在局域网里面使用,通过/etc/hosts文件,主机名可以解析到对应的ip;域名通常是再internet上使用。

众所周知,百度的域名为:www.baidu.com,而这个域名其实对应了一个百度公司的IP地址,那么百度公司的IP地址是多少呢?我们可以利用ping www.baidu.com来得到百度公司的ip地址,如图。那么,系统是如何将www.baidu.com 这个域名转化为IP地址220.181.111.148的呢?
这里写图片描述
在linux中,有一些函数可以实现主机名和地址的转化,最常见的有gethostbyname()、gethostbyaddr()等,它们都可以实现IPv4和IPv6的地址和主机名之间的转化。其中gethostbyname()是将主机名转化为IP地址,gethostbyaddr()则是逆操作,是将IP地址转化为主机名。
函数原型:

#include <netdb.h>
struct hostent* gethostbyname(const char* hostname);
struct hostent* gethostbyaddr(const char* addr, size_t len, int family);

结构体:

struct hostent
{
    char *h_name;       /*正式主机名*/
    char **h_aliases;   /*主机别名*/
    int h_addrtype;     /*主机IP地址类型 IPv4为AF_INET*/
    int h_length;       /*主机IP地址字节长度,对于IPv4是4字节,即32位*/
    char **h_addr_list; /*主机的IP地址列表*/
}
#define  h_addr  h_addr_list[0] /*保存的是ip地址*/

1.函数gethostbyname():用于将域名(www.baidu.com)或主机名转换为IP地址。参数hostname指向存放域名或主机名的字符串。
2.函数gethostbyaddr():用于将IP地址转换为域名或主机名。
- 参数addr是一个IP地址,此时这个ip地址不是普通的字符串,而是要通过函数inet_aton()转换。
- len为IP地址的长度,AF_INET为4。
- family可用AF_INET:Ipv4或AF_INET6:Ipv6。

Example1:将百度的www.baidu.com 转换为ip地址

#include <netdb.h>
#include <sys/socket.h>
#include <stdio.h>
int main(int argc, char **argv)
{
    char *ptr, **pptr;
    struct hostent *hptr;
    char str[32] = {''};
    /* 取得命令后第一个参数,即要解析的域名或主机名 */
    ptr = argv[1];  //如www.baidu.com
    /* 调用gethostbyname()。结果存在hptr结构中 */
    if((hptr = gethostbyname(ptr)) == NULL)
    {
        printf(" gethostbyname error for host:%sn", ptr);
        return 0;
    }
    /* 将主机的规范名打出来 */
    printf("official hostname:%sn",hptr->h_name);
    /* 主机可能有多个别名,将所有别名分别打出来 */
    for(pptr = hptr->h_aliases; *pptr != NULL; pptr++)
        printf(" alias:%sn",*pptr);
    /* 根据地址类型,将地址打出来 */
    switch(hptr->h_addrtype)
    {
        case AF_INET:
        case AF_INET6:
            pptr=hptr->h_addr_list;
            /* 将刚才得到的所有地址都打出来。其中调用了inet_ntop()函数 */
            for(; *pptr!=NULL; pptr++)
            {
                //inet_ntop()第二个参数可以是char*或in_addr类型等
                inet_ntop(hptr->h_addrtype, *pptr, str, sizeof(str));
                printf("address:%sn",str);
            }
            inet_ntop(hptr->h_addrtype, *hptr->h_addr_list, str, sizeof(str));
            printf("The first address:%sn",str);
            break;
        default:
            printf("unknown address typen");
            break;
    }
    return 0;
}

(注:这里需要联网才能访问www.baidu.com。可以尝试用自己的虚拟机的主机名,利用命令hostname可以查看自己的主机名,用hostname –i可以查看主机名对应的ip地址。那么如何修改主机名呢?直接用hostname wangxiao只是暂时有效,重启之后就没有了,想要永久有效,需要修改/etc/sysconfig/network将HOSTNAME修改,当然要重启虚拟机。如果ip地址不对,你可以修改/etc/hosts这个文件,添加你自己的主机名对应的ip地址)

3. socket编程

3.1. 使用TCP协议的流程图

TCP通信的基本步骤如下:
服务端:socket—bind—listen—while(1){—accept—recv—send—close—}—close
客户端:socket———————————-connect—send—recv—————–close

这里写图片描述

3.1.1.服务器端

(1).头文件包含:

#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<string.h>
#include <stdio.h>
#include <stdlib.h>

(2). socket函数:生成一个套接口描述符。

原型:int socket(int domain,int type,int protocol);
参数:domain:AF_INET:Ipv4网络协议AF_INET6:IPv6网络协议
type:tcp:SOCK_STREAM udp:SOCK_DGRAM
protocol:指定socket所使用的传输协议编号。通常为0.
返回值:成功则返回套接口描述符,失败返回-1。

常用实例:

int sfd = socket(AF_INET, SOCK_STREAM, 0);
if(sfd == -1){perror("socket");exit(-1);}

(3).bind函数:用来绑定一个端口号和IP地址,使套接口与指定的端口号和IP地址相关联。

原型:int bind(int sockfd,struct sockaddr * my_addr,int addrlen);
参数:
- sockfd为前面socket的返回值。
- my_addr为结构体指针变量

对于不同的socket domain定义了一个通用的数据结构

struct sockaddr  //此结构体不常用
{
    unsigned short int sa_family;  //调用socket()时的domain参数,即AF_INET值。
    char sa_data[14];  //最多使用14个字符长度
};

此sockaddr结构会因使用不同的socket domain而有不同结构定义,
例如使用AF_INET domain,其socketaddr结构定义便为

struct sockaddr_in  //常用的结构体
{
    unsigned short int sin_family;  //即为sa_family AF_INET
    uint16_t sin_port;  //为使用的port编号
    struct in_addr sin_addr;  //为IP 地址
    unsigned char sin_zero[8];  //未使用
};
struct in_addr
{
    uint32_t s_addr;
};

addrlen表示sockaddr的结构体长度。通常是计算sizeof(struct sockaddr);
返回值:成功则返回0,失败返回-1
常用实例:

struct sockaddr_in my_addr;  //定义结构体变量
//或bzero(&my_addr, sizeof(struct sockaddr));
memset(&my_addr, 0, sizeof(struct sockaddr)); //将结构体清空
my_addr.sin_family = AF_INET;  //表示采用Ipv4网络协议
my_addr.sin_port = htons(8888);  //表示端口号为8888,通常是大于1024的一个值。
//htons()用来将参数指定的16位hostshort转换成网络字符顺序
my_addr.sin_addr.s_addr = inet_addr("192.168.0.101"); //inet_addr()用来将IP地址字符串转换成网络所使用的二进制数字,如果为INADDR_ANY,这表示服务器自动填充本机IP地址。
if(bind(sfd, (struct sockaddr*)&my_str, sizeof(struct socketaddr)) == -1)
{
    perror("bind");close(sfd);exit(-1);
}

(注:通过将my_addr.sin_port置为0,函数会自动为你选择一个未占用的端口来使用。同样,通过将my_addr.sin_addr.s_addr置为INADDR_ANY,系统会自动填入本机IP地址。)

(4). listen函数:使服务器的这个端口和IP处于监听状态,等待网络中某一客户机的连接请求。如果客户端有连接请求,端口就会接受这个连接。

原型:int listen(int sockfd,int backlog);
参数:
- sockfd为前面socket的返回值.即sfd
- backlog指定维持的监听队列大小,同时能处理的最大连接要求,通常为10或者5。最大值可设至128

返回值:成功则返回0,失败返回-1

常用实例:

if(listen(sfd, 10) == -1)
{
    perror("listen");
    close(sfd);
    exit(-1);
}

(5). accept函数:接受远程计算机的连接请求,建立起与客户机之间的通信连接。服务器处于监听状态时,如果某时刻获得客户机的连接请求,此时并不是立即处理这个请求,而是将这个请求放在等待队列中,当系统空闲时再处理客户机的连接请求。当accept函数接受一个连接时,会返回一个新的socket标识符,以后的数据传输和读取就要通过这个新的socket编号来处理,原来参数中的socket也可以继续使用,继续监听其它客户机的连接请求。(也就是说,类似于移动营业厅,如果有客户打电话给10086,此时服务器就会请求连接,处理一些事务之后,就通知一个话务员接听客户的电话,也就是说,后面的所有操作,此时已经于服务器没有关系,而是话务员跟客户的交流。对应过来,客户请求连接我们的服务器,我们服务器先做了一些绑定和监听等等操作之后,如果允许连接,则调用accept函数产生一个新的套接字,然后用这个新的套接字跟我们的客户进行收发数据。也就是说,服务器跟一个客户端连接成功,会有两个套接字。)

原型:int accept(int s,struct sockaddr * addr,int * addrlen);
参数:
- s为前面socket的返回值.即sfd
- addr为结构体指针变量,和bind的结构体是同种类型的,系统会把远程主机的信息(远程主机的地址和端口号信息)保存到这个指针所指的结构体中。
- addrlen表示结构体的长度,为整型指针

返回值:成功则返回新的socket处理代码new_fd,失败返回-1

常用实例:

struct sockaddr_in clientaddr;
memset(&clientaddr, 0, sizeof(struct sockaddr));
int addrlen = sizeof(struct sockaddr);
int new_fd = accept(sfd, (struct sockaddr*)&clientaddr, &addrlen);
if(new_fd == -1)
{
    perror("accept");
    close(sfd);exit(-1);
}
printf("%s %d success connectn",inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port));

(6). recv函数:用新的套接字来接收远端主机传来的数据,并把数据存到由参数buf 指向的内存空间

原型:int recv(int sockfd,void *buf,int len,unsigned int flags);
参数:
- sockfd为前面accept的返回值.即new_fd,也就是新的套接字。
- buf表示缓冲区
- len表示缓冲区的长度
- flags通常为0

返回值:成功则返回实际接收到的字符数,如果对方退出的话,返回零。可能会少于你所指定的接收长度。失败返回-1

常用实例:

char buf[512] = {0};
if(recv(new_fd, buf, sizeof(buf), 0) == -1)
{
    perror("recv");
    close(new_fd);
    close(sfd);
    exit(-1);
}
puts(buf);

(7). send函数:用新的套接字发送数据给指定的远端主机

原型:int send(int s,const void * msg,int len,unsigned int flags);
参数:
- s为前面accept的返回值.即new_fd
- msg一般为常量字符串
- len表示长度
- flags通常为0

返回值:成功则返回实际传送出去的字符数,可能会少于你所指定的发送长度。失败返回-1

常用实例:

if(send(new_fd, "hello", 6, 0) == -1)
{
    perror("send");
    close(new_fd);
    close(sfd);
    exit(-1);
}

(8). close函数:当使用完文件后若已不再需要则可使用close()关闭该文件,并且close()会让数据写回磁盘,并释放该文件所占用的资源

原型:int close(int fd);
参数:fd为前面的sfd,new_fd
返回值:若文件顺利关闭则返回0,发生错误时返回-1

常用实例:

close(new_fd);
close(sfd);

3.1.2.客户端

(1). connect函数:用来请求连接远程服务器,将参数sockfd 的socket 连至参数serv_addr 指定的服务器IP和端口号上去。

原型:int connect (int sockfd,struct sockaddr * serv_addr,int addrlen);
参数:
- sockfd为前面socket的返回值,即sfd
- serv_addr为结构体指针变量,存储着远程服务器的IP与端口号信息。
- addrlen表示结构体变量的长度

返回值:成功则返回0,失败返回-1

常用实例:

struct sockaddr_in seraddr;//请求连接服务器
memset(&seraddr, 0, sizeof(struct sockaddr));
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(8888); //服务器的端口号
seraddr.sin_addr.s_addr = inet_addr("192.168.0.101");  //服务器的ip
if(connect(sfd, (struct sockaddr*)&seraddr, sizeof(struct sockaddr)) == -1)
{
    perror("connect");close(sfd);exit(-1);
}

将上面的头文件以及各个函数中的代码全部拷贝就可以形成一个完整的例子,此处省略。

3.1.3.一个阻塞的TCP服务器实例

将一些通用的代码全部封装起来,以后要用直接调用函数即可。如下:
通用网络封装代码头文件

tcp.h
#ifndef __TCP_H__
#define __TCP_H__

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<signal.h>
#include<arpa/inet.h>
#define IP "192.168.1.101"
#define PORT 1234

#endif

具体的通用函数封装如下:

tcp_func.c
#include "tcp.h"

int ftp_init()
{//socket(),bind(),listen(),返回socket描述符
    int sfd=socket(AF_INET,SOCK_STREAM,0);
    if(-1==sfd)
    {
        perror("sfd");
        exit(1);
    }
    //
    struct sockaddr_in server_addr;
    server_addr.sin_family=AF_INET;
    server_addr.sin_port=htons(PORT);
    server_addr.sin_addr.s_addr=inet_addr(IP);
    int addr_len=sizeof(struct sockaddr);
    //绑定
    if(-1==bind(sfd,(struct sockaddr*)&server_addr,(socklen_t)addr_len))
    {
        perror("bind");
        close(sfd);
        exit(1);
    }
    //监听
    if(-1==listen(sfd,5))
    {
        perror("listen");
        close(sfd);
        exit(1);
    }
    return sfd;
}

int ftp_accept(int sfd)
{//同意connect()
    struct sockaddr_in client_addr;
    memset(&client_addr,0,sizeof(struct sockaddr_in));
    client_addr.sin_family=AF_INET;
    client_addr.sin_port=htons(PORT);
    client_addr.sin_addr.s_addr=inet_addr(IP);
    int len=sizeof(struct sockaddr);
    //
    int sfd_new=accept(sfd,(struct sockaddr*)&client_addr,(socklen_t*)&len);
    if(-1==sfd_new)
    {
        perror("accept");
        close(sfd);
        close(sfd_new);
        exit(1);
    }
    //
    printf("%s %d success connect...n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));
    return sfd_new;
}

int tcp_connect()
{//客户端连接
    //生成socket描述符
    int sfd=socket(AF_INET,SOCK_STREAM,0);
    if(-1==sfd)
    {
        perror("socket");
        exit(1);
    }
    //
    struct sockaddr_in server_addr;
    server_addr.sin_family=AF_INET;
    server_addr.sin_port=htons(PORT);
    server_addr.sin_addr.s_addr=inet_addr(IP);
    int len=sizeof(struct sockaddr);
    //连接
    printf("connecting...n");
    if(-1==connect(sfd,(const struct sockaddr*)&server_addr,(socklen_t)len))
    {
        perror("connect");
        close(sfd);
        exit(1);
    }
    printf("connected success!n");
    return sfd;
}

void signal_handle(void)
{//ctrl+c ctrl+d信号处理
/*
    sigset_t sigset;
    sigemptyset(&sigset);
    sigaddset(&sigset,SIGINT);
    sigaddset(&sigset,SIGQUIT);
    sigprocmask(SIG_BLOCK,&sigset,NULL);
*/
}

服务器端:

tcp_server.c
#include "tcp.h"

int main(int argc,char* argv[])
{
    signal_handle();
    int sfd=ftp_init();
    int sfd_new=ftp_accept(sfd);
    char buf[128];
    int ret;
    while(1)
    {
        memset(buf,0,sizeof(buf));
        ret=recv(sfd_new,buf,sizeof(buf),0);
        if(-1==ret)
        {
            perror("recv");
            close(sfd);
            close(sfd_new);
            exit(1);
        }
        if(0==ret)
        {//如果客户端ctrl+c,ctrl+d,或者enter,即recv()没接收到字符
            break;
        }
        printf("from client:%sn",buf);

        memset(buf,0,sizeof(buf));
        if(-1==read(0,buf,sizeof(buf)))
        {
            perror("read");
            close(sfd);
            close(sfd_new);
            exit(1);
        }
        if(-1==send(sfd_new,buf,sizeof(buf),0))
        {
            perror("send");
            close(sfd);
            close(sfd_new);
            exit(1);
        }
    }
    close(sfd);
    close(sfd_new);
    return 0;
}

客户端:

tcp_client.c
#include "tcp.h"

int main(int argc,char* argv[])
{
    int sfd=tcp_connect();
    char buf[128];
    while(1)
    {
        memset(buf,0,sizeof(buf));
        if(-1==read(0,buf,sizeof(buf)))
        {
            perror("read");
            close(sfd);
            exit(1);
        }
        if(-1==send(sfd,buf,sizeof(buf),0))
        {
            perror("send");
            close(sfd);
            exit(1);
        }
        memset(buf,0,sizeof(buf));
        if(-1==recv(sfd,buf,sizeof(buf),0))
        {
            perror("recv");
            close(sfd);
            exit(1);
        }
        printf("from server:%sn",buf);
    }
    close(sfd);
    return 0;
}
编译

#gcc –o server tcp_server.c tcp_func.c
#gcc –o tcp_client tcp_client.c tcp_func.c
#./server
#./client
/* 备注
可以通过

#gcc –fpic –c tcp_func.c –o tcp_func.o
#gcc –shared tcp_func.o –o libtcp_func.so
#cp lib*.so /lib

这样以后就可以直接使用该库了
cp tcp.h /usr/include/ //这样头文件包含可以用include

3.1.4把3.1.3实例改为非阻塞的解决办法

(1). 多进程(因为开销比较大,所以不常用)

int main(int argc, char* argv[])
{
    if(argc < 3)
    {
        printf("usage:./servertcp  ip  portn");
        exit(-1);
    }
    int sfd = tcp_init(argv[1], atoi(argv[2]));
    char buf[512] = {0};
    while(1)
    {
        int cfd = tcp_accept(sfd);
        if(fork() == 0)
        {
            recv(cfd,buf,sizeof(buf),0);
            puts(buf);
            send(cfd,"hello",6,0);
            close(cfd);
        }
        else
        {
            close(cfd);
        } 
    }
    close(sfd);
}

(2). 多线程
将服务器上文件的内容全部发给客户端

/* TCP 文件服务器演示代码 */
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/fcntl.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <pthread.h>

#define DEFAULT_SVR_PORT 2828
#define FILE_MAX_LEN 64
char filename[FILE_MAX_LEN+1];

static void * handle_client(void * arg)
{
        int sock = (int)arg;
        char buff[1024];
        int len ;
        printf("begin sendn");
        FILE* file = fopen(filename,"r");
        if(file == NULL)
        {
            close(sock);
            exit;
        }
        //发文件名
        if(send(sock,filename,FILE_MAX_LEN,0) == -1)
        {
            perror("send file namen");
            goto EXIT_THREAD;
        }
        printf("begin send file %s....n",filename);
     //发文件内容
        while(!feof(file))
        {
            len = fread(buff,1,sizeof(buff),file);
            printf("server read %s,len %dn",filename,len);
            if(send(sock,buff,len,0) < 0)
            {
                perror("send file:");
                goto EXIT_THREAD;
            }
        }
EXIT_THREAD:
        if(file)
            fclose(file);
        close(sock);
}

int main(int argc,char * argv[])
{
        int sockfd,new_fd;
        //第1.定义两个ipv4 地址
        struct sockaddr_in my_addr;
        struct sockaddr_in their_addr;
        int sin_size,numbytes;
        pthread_t cli_thread;
        unsigned short port;
        if(argc < 2)
        {
            printf("need a filename without pathn");
            exit;
        }
        strncpy(filename,argv[1],FILE_MAX_LEN);
        port = DEFAULT_SVR_PORT;
        if(argc >= 3)
        {
            port = (unsigned short)atoi(argv[2]);
        }
        //第一步:建立TCP套接字 Socket
        // AF_INET --> ip通讯
        //SOCK_STREAM -->TCP
        if((sockfd = socket(AF_INET,SOCK_STREAM,0)) == -1) 
        {
            perror("socket");
            exit(-1);
        }
        //第二步:设置侦听端口
        //初始化结构体,并绑定2828端口
        memset(&my_addr,0,sizeof(struct sockaddr));
        //memset(&my_addr,0,sizeof(my_addr));
        my_addr.sin_family = AF_INET;     /* ipv4 */ 
        my_addr.sin_port = htons(port);   /* 设置侦听端口是 2828 , 用htons转成网络序*/
        my_addr.sin_addr.s_addr = INADDR_ANY;/* INADDR_ANY来表示任意IP地址可能其通讯 */
        //bzero(&(my_addr.sin_zero),8);
        //第三步:绑定套接口,把socket队列与端口关联起来.
        if(bind(sockfd,(struct sockaddr*)&my_addr,sizeof(struct sockaddr)) == -1)
        {
            perror("bind");
            
                            
                            版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/he_0123/article/details/50457612
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2021-06-12 18:36:47
  • 阅读 ( 1530 )
  • 分类:Linux

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢