linux Socket-应用编程-专题讲座
                            written by 王保明
Socket编程基本实践
1Socket Api基本概念
什么是socket?
socket可以看成是用户进程与内核网络协议栈的编程接口。
socket不仅可以用于本机的进程间通信,还可以用于网络上不同主机的进程间通信。
tcp/ip通讯模型
IPv4套接口地址结构
IPv4套接口地址结构通常也称为“网际套接字地址结构”,它以“sockaddr_in”命名,定义在头文件<netinet/in.h>中
struct sockaddr_in {
uint8_t  sin_len; 4
sa_family_t  sin_family; 4
in_port_t    sin_port; 2
struct in_addr    sin_addr; 4
char sin_zero[8]; 8
};
sin_len:整个sockaddr_in结构体的长度,在4.3BSD-Reno版本之前的第一个成员是sin_family.
sin_family:指定该地址家族,在这里必须设为AF_INET
sin_port:端口
sin_addr:IPv4的地址;
sin_zero:暂不使用,一般将其设置为0
通用地址结构
通用地址结构用来指定与套接字关联的地址。
struct sockaddr {
    uint8_t  sin_len;
    sa_family_t  sin_family;
    char sa_data[14]; //14
};
sin_len:整个sockaddr结构体的长度
sin_family:指定该地址家族
sa_data:由sin_family决定它的形式。
网络字节序
字节序
        大端字节序(Big Endian)
最高有效位(MSB:Most Significant Bit)存储于最低内存地址处,最低有效位(LSB:Lowest Significant Bit)存储于最高内存地址处。
小端字节序(Little Endian)
最高有效位(MSB:Most Significant Bit)存储于最高内存地址    处,最低有效位(LSB:Lowest Significant Bit)存储于最低内存地址处。
主机字节序
不同的主机有不同的字节序,如x86为小端字节序,Motorola 6800为大端字节序,ARM字节序是可配置的。
网络字节序
网络字节序规定为大端字节序
字节序转换函数
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
说明:在上述的函数中,h代表host;n代表network s代表short;l代表long
地址转换函数
#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);
套接字类型
流式套接字(SOCK_STREAM)
    提供面向连接的、可靠的数据传输服务,数据无差错,无重复的发送,且按发送顺序接收。
数据报式套接字(SOCK_DGRAM)
    提供无连接服务。不提供无错保证,数据可能丢失或重复,并且接收顺序混乱。
原始套接字(SOCK_RAW)
2SocketApi基本编程模型
TCP客户/服务器模型
简单服务器模型
3Socket Api基本实践
Socket API基本用法
socket函数
包含头文件<sys/socket.h>
功能:创建一个套接字用于通信
原型
int socket(int domain, int type, int protocol);
参数
domain :指定通信协议族(protocol family)
type:指定socket类型,流式套接字SOCK_STREAM,数据报套接字SOCK_DGRAM,原始套接字SOCK_RAW
protocol :协议类型
返回值:成功返回非负整数, 它与文件描述符类似,我们把它称为套接口描述字,简称套接字。失败返回-1
bind函数
包含头文件<sys/socket.h>
功能:绑定一个本地地址到套接字
原型
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数
sockfd:socket函数返回的套接字
addr:要绑定的地址
addrlen:地址长度
返回值:成功返回0,失败返回-1
listen函数
一般来说,listen函数应该在调用socket和bind函数之后,调用函数accept之前调用。
对于给定的监听套接口,内核要维护两个队列:
1、已由客户发出并到达服务器,服务器正在等待完成相应的TCP三路握手过程
2、已完成连接的队列
accept函数
包含头文件<sys/socket.h>
功能:从已完成连接队列返回第一个连接,如果已完成连接队列为空,则阻塞。
原型
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数
sockfd:服务器套接字
addr:将返回对等方的套接字地址
addrlen:返回对等方的套接字地址长度
返回值:成功返回非负整数,失败返回-1
connect函数
包含头文件<sys/socket.h>
功能:建立一个连接至addr所指定的套接字
原型
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数
sockfd:未连接套接字
addr:要连接的套接字地址
addrlen:第二个参数addr长度
返回值:成功返回0,失败返回-1
Socket API 中的地址复用
SO_REUSEADDR
服务器端尽可能使用SO_REUSEADDR
在绑定之前尽可能调用setsockopt来设置SO_REUSEADDR套接字选项。
使用SO_REUSEADDR选项可以使得不必等待TIME_WAIT状态消失就可以重启服务器
    int optval = 1;
    if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0)
    {
        perror("setsockopt bind\n");
        exit(0);
    }
Socket服务支持多并发(多客户端连接) 
分析最基本socket服务器客户端模型能否支持多客户端连接
点对点聊天程序设计与实现
点对点聊天程序,功能说明
点对点聊天程序,设计思想
注意:客户端退出、服务器端退出,对各自的影响;分析父子进程生命周期关系。
注意:体会长链接和短连接。。。。
客户端实现思路:
void myhanldle(int sig)
{
    printf("resv sig:%d 客户端父进程也退出\n", sig);
    exit(0);
}
int main(void )
{
    int    sock;
   
    sock = socket(PF_INET, SOCK_STREAM, 0);
    //listenfd = socket(AF_INET,  IPPROTO_TCP, 0);
    if (sock < 0)
    {
        perror("socket:");
        exit(0);
    }
   
    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(8089);
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //绑定服务器任意一个i
    //inet_aton("127.0.0.1", &servaddr.sin_addr);*/
   
    if ( connect(sock, (struct sockaddr *)&servaddr,  sizeof(servaddr)) < 0)
    {
        perror("connect:");
        exit(0);
    }
    pid_t pid;
    pid = fork();
    if (pid == -1)
    {
        perror("fork err:");
        close(sock);
        exit(0);
    }
    if (pid == 0)
    {
        char recvbuf[1024] = {0};
        while (1)
        {
            memset(recvbuf, 0, sizeof(recvbuf));
            int ret = read(sock, recvbuf, sizeof(recvbuf));
            if (ret == -1)
            {
                perror("read err \n");
                break;
            }
            else if (ret == 0)
            {
                printf("peer close \n");
                break;
            }
            fputs(recvbuf, stdout);
        }
        close(sock);
        //客户端检测到对方退出以后,发信号给父进程退出
        kill(getppid(), SIGUSR1);
       
        printf("客户端子进程退出\n");
        exit(0);
    }
    else
    {
        char sendbuf[1024] = {0};
        signal(SIGUSR1, myhanldle);
        printf("客户端父进程要求输入数据:\n");
        while ( fgets(sendbuf, sizeof(sendbuf), stdin ) != NULL )
        {
            printf("客户端获取数据长度:%d \n", strlen(sendbuf));
            write(sock, sendbuf, strlen(sendbuf));
            memset(sendbuf, 0, sizeof(sendbuf));
        }
        printf("客户端父进程退出\n");
        printf("parent close\n");
   
        //exit(EXIT_SUCCESS);
    }
   
    //close(sock);
    return 0;
}
服务器端实现思路:
void mysighandler_t(int sig)
{
    printf("child resv sig: %d ,child quit\n", sig);
    exit(0);   
}
int main(void )
{
    int    listenfd;
   
    listenfd = socket(PF_INET, SOCK_STREAM, 0);
    //listenfd = socket(AF_INET,  IPPROTO_TCP, 0);
    if (listenfd < 0)
    {
        printf("func socket() err\n");
        exit(0);
    }
   
    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(8089);
    //servaddr.sin_addr.s_addr = inet_addr(INADDR_ANY); 绑定服务器任意一个ip
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //绑定服务器任意一个ip
    //inet_aton("127.0.0.1", &servaddr.sin_addr);*/
   
    int  optval = 1;
    //每个级别SOL_SOCKET下,也有很多选项 不同的选择会有不同的结构
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0)
    {
        perror("setsockopt");
        exit(0);
    }
   
    if ( bind(listenfd, (struct sockaddr * )&servaddr, sizeof(servaddr)) < 0 )
    {
        perror("bind");
        exit(0);
    }
   
    if ( listen(listenfd, SOMAXCONN) < 0)
    {
        perror("listen");
        exit(0);
    }    socket通信报文格式
   
    int conn;
   
    struct sockaddr_in peeraddr;
    socklen_t addrlen;
    memset(&peeraddr, 0, sizeof(struct sockaddr_in ));
    addrlen =  sizeof(struct sockaddr_in );
   
    conn = accept(listenfd, (struct sockaddr *)&peeraddr, &addrlen );
    if (conn < 0)
    {
        perror("accept");
        exit(0);
    }
   
    printf("perradd:%s peerport:%d \n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
   
    int pid = fork ();
    if (pid == -1)
    {
        perror("fork err:");    //创建子进程失败
        close(conn); //关闭conn套接字
        close(listenfd); //关闭监听套接字
        exit(0);
    }
    close(listenfd); //关闭监听套接字
   
    if (pid == 0)
    {
        signal(SIGUSR1, mysighandler_t);
       
       
        char sendbuf[1024] = {0};
        while ( fgets(sendbuf, sizeof(sendbuf), stdin) !=NULL )
        {
            write(conn, sendbuf, strlen(sendbuf));
            memset(sendbuf, 0, sizeof(sendbuf));
        }
        printf("child close\n");
        exit(EXIT_SUCCESS);
    }
    else if (pid > 0)
    {
        char recbuf[1024];
        while (1)
        {
            memset(recbuf, 0, sizeof(recbuf));
            //如果对方退出,捕捉
            int ret = read(conn, recbuf, sizeof(recbuf));
            if (ret == 0)
            {
                printf("ret == 0 peer close退出\n");
                break;  //去掉做实验
            }
            else if (ret < 0)
            {
                perror("ret<0");
                break;
            }
            fputs(recbuf, stdout);
        }
        kill(pid, SIGUSR1);
        printf("parent quit\n");
        close(conn);
        exit(0);
    }
    return 0;
}
4Socket Api编程进价
1流协议与粘包
流协议与粘包
c
粘包产生的原因
说明
tcp    字节流 无边界
udp    消息、数据报 有边界
对等方,一次读操作,不能保证完全把消息读完。
对方接受数据包的个数是不确定的。
产生粘包问题的原因
    1、SQ_SNDBUF 套接字本身有缓冲区 (发送缓冲区、接收缓冲区)
    2、tcp传送的网络数据最大值MSS大小限制
    3、链路层也有MTU(最大传送单元)大小限制,如果数据包大于>MTU要在IP层进行分片,导致消息分割。(可以简单的认为MTU是MSS加包头数据。)
    4、tcp的流量控制和拥塞控制,也可能导致粘包
    5、tcp延迟发送机制 等等
结论:tcp/ip协议,在传输层没有处理粘包问题。
粘包解决方案 
本质上是要在应用层维护消息与消息的边界
定长包
包尾加\r\n(ftp)
包头加上包体长度
更复杂的应用层协议
2包头加上包体长度编程实践
编程实践
readn
written
发送数据:前4个字节代表报文长度(网络字节序)+报文内容
接受数据:先读4个字节,然后根据报文长度,再度内容。
//1一次全部读走 //2次读完数据 //出错分析 //对方已关闭
//思想:tcpip是流协议,不能保证1次读操作,能全部把报文读走,所以要循环读指定长度的数据。
//按照count大小读数据,
//若读取的长度ssize_t<count 说明读到了一个结束符,对方已关闭。
//@ssize_t:返回读的长度 若ssize_t<count 读失败失败
//@buf:接受数据内存首地址
//@count:接受数据长度
ssize_t readn(int fd, void *buf, size_t count)
{
    size_t nleft = count;
    ssize_t nread;
    char *bufp = (char*)buf;
    while (nleft > 0)
    {
        if ((nread = read(fd, bufp, nleft)) < 0)
        {
            if (errno == EINTR)
                continue;
            return -1;
        }
        else if (nread == 0) //若对方已关闭
            return count - nleft;
        bufp += nread;
        nleft -= nread;
    }
    return count;
}
//1一次全部读走 //2次读完数据 //出错分析 //对方已关闭
//思想:tcpip是流协议,不能1次把指定长度数据,全部写完
//按照count大小写数据
//若读取的长度ssize_t<count 说明读到了一个结束符,对方已关闭。
//@ssize_t:返回写的长度 -1失败
//@buf:待写数据首地址
//@count:待写长度
ssize_t writen(int fd, const void *buf, size_t count)
{
    size_t nleft = count;
    ssize_t nwritten;
    char *bufp = (char*)buf;
    while (nleft > 0)
    {
        if ((nwritten = write(fd, bufp, nleft)) < 0)
        {
            if (errno == EINTR)
                continue;
            return -1;
        }
        else if (nwritten == 0)
            continue;
        bufp += nwritten;
        nleft -= nwritten;
    }
    return count;
}
3包尾加上\n编程实践
\n作为协议的边界
ssize_t recv(int s, void *buf, size_t len, int flags);
与read相比,只能用于套接字文件描述符;
多了一个flags
      MSG_OOB
  This  flag requests receipt of out-of-band data that would not be received in the normal data stream.  Some protocols place expedited data at thehead of the normal data queue, and thus this flag cannot be used with such protocols.
带外数据 紧急指针
      MSG_PEEK
This flag causes the receive operation to return data from the beginning of the receive queue without removing that data from the queue.  Thus, asubsequent receive call will return the same data.
        可以读数据,不从缓存区中读走,利用此特点可以方便的实现按行读取数据。
    一个一个字符的读,方法不好;多次调用系统调用read方法
recv函数会将套接字缓冲区中的内容读出,但不清空,与read函数的区别在此。此函数有一个flag标志位,设为MSG_PEEK。
send函数会将缓冲区中的内容写入到套接字,也不清空,与write函数的区别在此。
用这两个函数可以先接收或发送缓冲区中的内容,然后再用readn(此时缓冲区中的内容依然存在)与write函数去继续判断换行符/n,对缓冲区内容实现换行输出。
参考例题:

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。