天天看点

《网络编程 —— socket编程实例》

一、socket简介

1、网络中进程间通信

本机进程使用进程号区别不同的进程进程间通信方式有管道、信号、消息队列、共享内存、信号量等。网络中进程间的通信首先需要识别进程所在主机在网络中的唯一标识即网络层的ip地址主机上的进程可以通过传输层的协议与端口号识别。

2、socket原理

    socket是应用层与tcp/ip协议族通信的中间软件抽象层是一种编程接口。socket屏蔽了不同网络协议的差异支持面向连接(transmission control protocol - tcpip)和无连接(user datagram protocol-udp 和 inter-network packet exchange-ipx)的传输协议。

《网络编程 —— socket编程实例》

二、socket通信的基础知识

1、网络字节序

主机字节序即内存中存储字节的方式分为大端序和小端序。何为大端、小端呢小端将低字节存储在低地址。大端将高字节存储在低字节。网络中在处理多字节顺序时一般采用大端序。在网络传输时需要把主机字节序转换到网络字节序常用的转换函数如下

#include <arpa/inet.h>

uint32_t htonl(uint32_t hostlong);

uint16_t htons(uint16_t hostshort);

uint32_t ntohl(uint32_t netlong);

uint16_t ntohs(uint16_t netshort);

2、数据结构

struct sockaddr {

    sa_family_t sa_family; /* address family, af_xxx */

    char sa_data[14]; /* 14 bytes of protocol address */

};

struct sockaddr_in {

    __kernel_sa_family_t sin_family; /* address family */

    __be16 sin_port; /* port number */

    struct in_addr sin_addr; /* internet address */

    /* pad to size of `struct sockaddr'. */

    unsigned char __pad[__sock_size__ - sizeof(short int) -

sizeof(unsigned short int) - sizeof(struct in_addr)];

/* internet address. */

struct in_addr {

    __be32 s_addr;

3、ip地址转换

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

    将cp所指的字符串ip地址转换成32位的网络字节序ip地址

in_addr_t inet_addr(const char *cp);

    将cp所指的字符串ip地址转换成32位的网络字节序ip地址返回

char *inet_ntoa(struct in_addr in);

    将32位网络字节序ip地址转换成点分十进制的字符串ip地址

4、地址结构使用

a、定义一个struct sockaddr_in类型的变量并清空

        struct sockaddr_in serveraddr;

    bzero(&serveraddr,  sizeof(serveraddr));

b、填充地址信息

    serveraddr.sin_family = af_inet;

    serveraddr.sin_port = htons(8080);

    serveraddr.sin_addr.s_addr = inet_addr("192.168.6.100");

c、将该变量强制转换为struct sockaddr类型在函数中使用

        bind(listenfd, (struct sockaddr *)&serveraddr,  sizeof(serveraddr));

5、tcp连接的建立

    tcp协议通过三个报文段完成连接的建立建立连接的过程称为三次握手(three-way handshake)。

        第一次握手建立连接时客户端发送syn包(syn=j)到服务器并进入syn_send状态等待服务器确认syn同步序列编号(synchronize sequence numbers)。

    第二次握手服务器收到syn包必须确认客户的synack=j+1同时自己也发送一个syn包syn=k即syn+ack包此时服务器进入syn_recv状态

第三次握手客户端收到服务器的syn+ack包向服务器发送确认包ack(ack=k+1)此包发送完毕客户端和服务器进入established状态完成三次握手。

    一个完整的三次握手是 请求---应答---再次确认。

《网络编程 —— socket编程实例》

当客户端调用connect时触发了连接请求向服务器发送了syn j包这时connect进入阻塞状态服务器监听到连接请求即收到syn j包调用accept函数接收请求向客户端发送syn k ack j+1这时accept进入阻塞状态客户端收到服务器的syn k ack j+1之后这时connect返回并对syn k进行确认服务器收到ack k+1时accept返回至此三次握手完毕连接建立。

6、tcp连接的断开

    终止一个连接要经过四次握手简称四次握手释放

《网络编程 —— socket编程实例》

tcp连接是全双工的每个方向都必须单独进行关闭。当一方完成数据发送任务后就能发送一个fin来终止这个方向的连接。收到一个 fin只意味着这一方向上没有数据流动一个tcp连接在收到一个fin后仍能发送数据。首先进行关闭的一方将执行主动关闭而另一方执行被动关闭。

a、客户端a发送一个fin用来关闭客户a到服务器b的数据传送

b、服务器b收到这个fin它发回一个ack确认序号为收到的序号加1。和syn一样一个fin将占用一个序号。

   c、服务器b关闭与客户端a的连接发送一个fin给客户端a。

   d、客户端a发回ack报文确认并将确认序号设置为收到序号加1。

    为什么建立连接协议是三次握手而关闭连接却是四次握手呢

     因为服务端的listen状态下的socket当收到syn报文的建连请求后它可以把ack和synack起应答作用而syn起同步作用放在一个报文里来发送。但关闭连接时当收到对方的fin报文通知时仅仅表示对方没有数据发送给你了但你所有的数据未必都全部发送给对方了你未必会马上会关闭socket你可能还需要发送一些数据给对方之后再发送fin报文给对方来表示你同意现在可以关闭连接了所以ack报文和fin报文多数情况下都是分开发送的。

    为什么time_wait状态还需要等2msl后才能返回到closed状态

        虽然双方都同意关闭连接了而且握手的4个报文也都协调和发送完毕按理可以直接回到closed状态就好比从syn_send状态到 establish状态那样但是因为我们必须要假想网络是不可靠的你无法保证你最后发送的ack报文会一定被对方收到因此对方处于 last_ack状态下的socket可能会因为超时未收到ack报文而重发fin报文所以这个time_wait状态的作用就是用来重发可能丢失的 ack报文。

7、getaddrinfo

int getaddrinfo(const char *node, const char *service,

                       const struct addrinfo *hints,

                       struct addrinfo **res);

    node:一个主机名域名或者地址串(ipv4点分十进制串或者ipv6的16进制串)

        service服务名可以是十进制的端口号可以是已定义服务名如ftp、http等

        hints可以是一个空指针也可以是一个指向某个 addrinfo结构体的指针调用者在这个结构中填入关于期望返回的信息类型的暗示。举例来说如果指定的服务既支持tcp也支持udp那么调用者可以把hints结构中的ai_socktype成员设置成sock_dgram使得返回的仅仅是适用于数据报套接口的信息。

        result本函数通过result指针参数返回一个指向addrinfo结构体链表的指针。

        返回值0——成功非0——出错

struct addrinfo {

               int              ai_flags;

               int              ai_family;//af_inet,af_inet6或者af_unspec

               int              ai_socktype;//sock_stream or sock_dgram

               int              ai_protocol;//0

               size_t           ai_addrlen;

               struct sockaddr *ai_addr;

               char            *ai_canonname;

               struct addrinfo *ai_next;

           };

ai_flags:

    ai_passive套接字地址用于监听绑定

    ai_canonname需要一个规范名而不是别名

    ai_v4mapped如果没有找到ipv6地址返回映射到ipv6格式的ipv4地址

    ai_addrconfig查询配置的地址类型ipv4或ipv6

    ai_numericserv以端口号返回服务

    ai_numerichost以数字格式返回主机地址

   gethostbyname函数仅支持ipv4

struct hostent *gethostbyname(const char *name);

struct hostent {

               char  *h_name;            /* official name of host */

               char **h_aliases;         /* alias list */

               int    h_addrtype;        /* host address type */

               int    h_length;          /* length of address */

               char **h_addr_list;       /* list of addresses */

           }

           #define h_addr h_addr_list[0] /* for backward compatibility */

    name主机名或域名

三、socket接口函数

socket编程的一般流程如下

《网络编程 —— socket编程实例》

1、socket

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

    创建一个socket

    domain即协议域又称为协议族family。常用的协议族有af_inet、af_inet6、af_local或称af_unixunix域socket、af_route等等。协议族决定了socket的地址类型在通信中必须采用对应的地址如af_inet决定了要用ipv4地址32位的与端口号16位的的组合、af_unix决定了要用一个绝对路径名作为地址。

    type指定socket类型。常用的socket类型有sock_stream、sock_dgram、sock_raw、sock_packet、sock_seqpacket等等。

    protocol指定协议。常用的协议有ipproto_tcp、ipptoto_udp、ipproto_sctp、ipproto_tipc等它们分别对应tcp传输协议、udp传输协议、stcp传输协议、tipc传输协议。当protocol为0时会自动选择type类型对应的默认协议。

2、bind

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

        把一个地址族中的特定地址赋给socket

        sockfd即socket描述字通过socket函数创建得到唯一标识一个socket。

        addrconst struct sockaddr *指针指向要绑定给sockfd的协议地址。

ipv4的协议地址结构

    sa_family_t    sin_family; /* address family: af_inet */

    in_port_t      sin_port;   /* port in network byte order */

    struct in_addr sin_addr;   /* internet address */

/* internet address. */struct in_addr {

    uint32_t       s_addr;     /* address in network byte order */

ipv6的协议地址结构

struct sockaddr_in6 {

    sa_family_t     sin6_family;   /* af_inet6 */

    in_port_t       sin6_port;     /* port number */

    uint32_t        sin6_flowinfo; /* ipv6 flow information */

    struct in6_addr sin6_addr;     /* ipv6 address */

    uint32_t        sin6_scope_id; /* scope id (new in 2.4) */

struct in6_addr {

    unsigned char   s6_addr[16];   /* ipv6 address */

3、listen

int listen(int sockfd, int backlog);

    设置sockfd套接字为监听套接字

    sockfd参数即为要监听的socket描述字

    backlog参数为相应socket可以排队的最大连接个数。

        socket函数创建的socket默认是一个主动类型的listen函数将socket变为被动类型的等待客户的连接请求。

4、accept

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

接收客户端的请求建立连接套接字

    参数sockfd是监听套接字用来监听一个端口

    参数addr用来接收客户端的协议地址可以设置为null。

    参数len表示接收的客户端的协议地址addr结构的大小的可设置为null

    如果accept成功返回则服务器与客户已经正确建立连接了服务器通过accept返回的套接字来完成与客户的通信。

    accept默认会阻塞进程直到有一个客户连接建立后返回返回的是一个新可用的连接套接字。

    监听套接字: 在调用listen函数之后socket函数生成的主动连接的普通套接字就转变为监听套接字一般被accept函数调用的sockfd就是监听套接字

    连接套接字accept函数返回的是连接套接字代表与客户端已经建立连接

     一个服务器程序通常只创建一个监听套接字在服务器程序的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个连接套接字当服务器完成了对某个客户的服务相应的连接套接字就被关闭。

     连接套接字并没有占用新的端口与客户端通信依然使用的是与监听套接字sockfd一样的端口号。

5、connect

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

sockfd参数即为客户端的socket描述字

addr参数为服务器的socket地址

addrlen参数为socket地址的长度

成功执行后客户端通过调用connect函数来建立与tcp服务器的连接。

6、数据传输操作

ssize_t read(int fd, void *buf, size_t count);

        read函数是负责从连接套接字fd中读取内容。

    fd参数是accept函数建立的连接套接字

    buf参数是读取的内容存放的内存缓冲区

    count参数是要读取的内容的大小

    当读成功时read返回实际所读的字节数如果返回的值是0表示已经读到文件的结束小于0表示出现了错误。如果错误为eintr说明读是由中断引起的如果是econnrest表示网络连接出了问题。

ssize_t write(int fd, const void *buf, size_t count);

    write函数是向连接套接字fd写入内容

    fd参数表示建立的连接套接字

    buf参数表示要写入内容所在的内存缓冲区

    count参数表示要写入的内容的大小

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

    send函数向连接套接字sockfd发送内容

    sockfd参数表示发送到的连接套接字

    buf参数表示要发送的内容所在的内存缓冲区

    len参数表示要发送内容的长度

    flags参数表示send的标识符一般为0

    成功返回实际发送的字节数出错返回-1

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

    recv函数从连接套接字sockfd接收内容

    sockfd参数表示从哪个连接套接字接收内容

    buf参数表示接收的内容存放的内存缓冲区

    len参数表示接收内容的实际字节数

    flags参数表示recv操作标识符一般为0

ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

    sendmsg函数向连接套接字sockfd发送信息

    sockfd参数表示向哪个连接套接字发送信息

    msg参数表示要发送的信息的内存缓冲区

    flags参数表示sendmsg函数操作的标识一般为0msg_dontwait表示非阻塞模式msg_oob表示发送带外数据

struct msghdr {

               void         *msg_name;       /* optional address */

               socklen_t     msg_namelen;    /* size of address */

               struct iovec *msg_iov;        /* scatter/gather array */

               size_t        msg_iovlen;     /* # elements in msg_iov */

               void         *msg_control;    /* ancillary data, see below */

               socklen_t     msg_controllen; /* ancillary data buffer len */

               int           msg_flags;      /* flags on received message */

ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

    recvmsg函数从连接套接字scokfd接收信息

    sockfd参数表示从哪个连接套接字接收信息

    msg参数表示接收的信息存放的内存缓冲区

    flags参数表示recvmsg函数操作的标识符

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,

                      const struct sockaddr *dest_addr, socklen_t addrlen);

    sendto函数表示向连接套接字发送内容

    buf参数表示发送的内容所在的内存缓冲区

    len参数表示发送的信息的字节数

    flags参数表示sendto函数的操作标识符

    dest_addr参数表示发送到的地址的指针

    addrlen参数表示发送到的地址的长度

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,

                        struct sockaddr *src_addr, socklen_t *addrlen);

    recvfrom函数从连接套接字sockfd接收信息

    buf参数表示接收的信息存放的内存缓冲区

    len参数表示接收的实际字节数

    flags参数表示recvfrom函数的操作标识符

    src_addr参数表示接收的信息来自的主机协议地址所存放的内存缓冲区

    addrlen参数表示接收信息的源主机协议地址的长度

7、close

int close(int fd);

    关闭断开连接套接字

    fd参数表示要断开的连接套接字

四、程序实例

服务端server.c

客户端client.c