Socket及基本应用(TCP)

1、什么是Socket(插座、套接字)

  • IP:网络中能标识唯一一台主机;
  • Port:主机中标识唯一一个进程;
  • Socket:IP+Port;
  • socket在Linux中为七种基本文件中的一个(普通-、目录d、连接l、管道p、字符设备c、块设备b、套接字s)

2、Socket的基本特性

  • socket是成对出现,绑定ip+端口
  • 一个文件描述符,两个缓冲区,全双工读写

3、C/S模型

  • 大小端问题:
    • 小端:低地址-存低位、高地址-存高位
    • 大端:低地址-存高位、高地址-存低位
  • 网络数据流应采用大端字节序,即低地址高字节
    #include <arpa/inet.h>
    // h:host、n:network、l:表示32位长整数、s:表示16位短整数
    
    uint32_t htonl(uint32_t hostlong);
    uint16_t htons(uint16_t hostshort);
    uint32_t ntohl(uint32_t netlong);
    uint16_t ntohs(uint16_t netshort);
    
  • ip地址相关转换函数
    #include <arpa/inet.h>
    // 主机到网络
    int inet_pton(int af, const char *src, void *dst);
    // 网络到主机
    const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
    

4、Socket数据结构与相关t函数

// man 7 ip查看
struct sockaddr_in {
    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 */
};


#include <sys/socket.h>

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

       socket()  creates an endpoint for communication and returns a file descriptor that refers to that endpoint.  The file descriptor returned by a successful call will be the lowest-numbered file descriptor not currently open for the process.

       The domain argument specifies a communication domain; this selects the protocol family which will be used for communication.   These  families  are defined in <sys/socket.h>.  The currently understood formats include:

       Name                Purpose                          Man page
       AF_UNIX, AF_LOCAL   Local communication              unix(7)
       AF_INET             IPv4 Internet protocols          ip(7)
       AF_INET6            IPv6 Internet protocols          ipv6(7)
       AF_IPX              IPX - Novell protocols
       AF_NETLINK          Kernel user interface device     netlink(7)
       AF_X25              ITU-T X.25 / ISO-8208 protocol   x25(7)
       AF_AX25             Amateur radio AX.25 protocol
       AF_ATMPVC           Access to raw ATM PVCs
       AF_APPLETALK        AppleTalk                        ddp(7)
       AF_PACKET           Low level packet interface       packet(7)
       AF_ALG              Interface to kernel crypto API

       The socket has the indicated type, which specifies the communication semantics.  Currently defined types are:

       SOCK_STREAM     Provides sequenced, reliable, two-way, connection-based byte streams.  An out-of-band data transmission mechanism may be supported.

       SOCK_DGRAM      Supports datagrams (connectionless, unreliable messages of a fixed maximum length).

       SOCK_SEQPACKET  Provides  a  sequenced, reliable, two-way connection-based data transmission path for datagrams of fixed maximum length; a consumer is required to read an entire packet with each input system call.

       SOCK_RAW        Provides raw network protocol access.

       SOCK_RDM        Provides a reliable datagram layer that does not guarantee ordering.

       SOCK_PACKET     Obsolete and should not be used in new programs; see packet(7).

       Some socket types may not be implemented by all protocol families.

       Since Linux 2.6.27, the type argument serves a second purpose: in addition to specifying a socket type, it may include the bitwise OR of any of the following values, to modify the behavior of socket():

       SOCK_NONBLOCK   Set the O_NONBLOCK file status flag on the new open file description.  Using this flag saves extra calls to fcntl(2) to achieve the same result.

       SOCK_CLOEXEC    Set the close-on-exec (FD_CLOEXEC) flag on the new file descriptor.  See the description of the O_CLOEXEC flag in open(2) for  reasons why this may be useful.

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

struct sockaddr {
    sa_family_t sa_family;
    char        sa_data[14];
}

// 同时建立连接的数(处于3次握手的链接数和)
int listen(int sockfd, int backlog);

// struct sockaddr *addr为传出参数,socklen_t*addrlen为传入传出参数,返回一个新的socket文件描述符
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

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

5、Socket通讯的基本步骤

  • Server
    • socket( ) —> bind( ) —> listen( ) —> accept( ) —> read( ) —> write( ) —> read( ) —> close( )
  • Client
    • socket( ) —> connect( ) —> write( ) —> read( ) —> close( )

6、示例Demo

server.c

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <strings.h>

#define SOCKET_PORT 3000

int main(){

    // 1、socket
    int sfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sfd < 0){
        perror("socket error");
        exit(1);
    }
    // 2、bind
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(SOCKET_PORT);
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    int ret = bind(sfd, (struct sockaddr *)&addr, sizeof(addr));
    if (ret < 0){
        perror("bind error");
        exit(1);
    }
    // 3、listen
    ret = listen(sfd, 128);
    if (ret < 0){
        perror("listen error");
        exit(1);
    }
    // 4、accept
    struct sockaddr_in c_addr;
    socklen_t addrlen = sizeof(struct sockaddr_in);
    int cfd = accept(sfd, (struct sockaddr *)&c_addr, &addrlen);

    // 打印客户端信息
    char c_addr_s[BUFSIZ];
    bzero(c_addr_s, sizeof(c_addr_s));
    printf("Client:%s, port:%d\n",
            inet_ntop(AF_INET, &c_addr.sin_addr.s_addr, c_addr_s, sizeof(c_addr_s)),
            ntohs(c_addr.sin_port));

    // 5、read
    char buf[BUFSIZ];
    while(1){
        ssize_t n = read(cfd, buf, BUFSIZ);
        // 5.1、处理
        int i;
        for (i = 0; i < n; i++){
            buf[i] = toupper(buf[i]);
        }
        // 6、write
        ret = write(cfd, buf, n);
        if (ret < 0){
            perror("write error");
            exit(1);
        }
    }
    // 7、close
    close(sfd);
    close(cfd);
    return 0;
}

client.c

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

#define SERVER_PORT 3000
#define SERVER_IP "10.211.55.6"
int main(){
    // 1、socket
    int sfd = socket(PF_INET, SOCK_STREAM, 0);
    if (sfd < 0){
        perror("socket error");
        exit(1);
    }
    // 2、connect
    // 创建套接字结构体
    struct sockaddr_in svr_addr;
    svr_addr.sin_family = PF_INET;
    svr_addr.sin_port = htons(SERVER_PORT);
    // 清0操作
    memset(&svr_addr.sin_addr.s_addr, 0, sizeof(svr_addr.sin_addr.s_addr));
    // 将点分十进制字符串转为字节序
    inet_pton(PF_INET, SERVER_IP, &svr_addr.sin_addr.s_addr);
    // 发起网络连接
    int ret = connect(sfd, (struct sockaddr *)&svr_addr, sizeof(svr_addr));
    if (ret < 0){
        perror("connect error");
        exit(1);
    }else{
        printf("服务器连接成功!\n");
    }
    char buf[BUFSIZ];
    while(1){
        // 读取输入流
        fgets(buf, sizeof(buf), stdin);
        // 写入套接字
        write(sfd, buf, strlen(buf));
        // 读取服务器响应
        int n = read(sfd, buf, sizeof(buf));
        // 将结果输出到标准输出流
        write(STDOUT_FILENO, buf, n);
    }
    close(sfd);
    return 0;
}

执行结果:

// 服务端
parallels@ubuntu:~/Linux/socket$ ./server.out
Client:10.211.55.2, port:59929
^C
parallels@ubuntu:~/Linux/socket$

// 客户端
yusian@SianMac2:~/Linux/socket$ ./client.out
服务器连接成功!
abc
ABC
hello world
HELLO WORLD
^C
yusian@SianMac2:~/Linux/socket$
  • socket常用函数的封装

wrap.h

#IFNDEF _WRAP_H_
#DEFINE _WRAP_H_

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>

int Socket(int domain, int type, int protocol);
int Bind(int sockfd, const struct sockaddr_in *addr, socklen_t addrlen);
int Listen(int sockfd, int backlog);
int Accept(int sockfd, struct sockaddr_in *addr, socklen_t *addrlen);
int Connect(int sockfd, const struct sockaddr_in *addr, socklen_t addrlen);

#ENDIF

wrap.c

#include “wrap.h"
#include <errno.h>

void print_err(const char* descript)
{
    perror(descript);
    exit(1);
}

int Socket(int domain, int type, int protocol)
{
    int ret = socket(domain, type, protocol);
    if (ret < 1) print_err("socket error");
    return ret;
}

int Bind(int sockfd, const struct sockaddr_in *addr, socklen_t addrlen)
{
    int ret = bind(sockfd, (struct sockaddr *)addr, addrlen);
    if (ret < 0) print_err("bind error");
    return ret;
}

int Accept(int sockfd, struct sockaddr_in *addr, socklen_t *addrlen)
{
    int sfd;
again:
    sfd = accept(sockfd, (struct sockaddr *)addr, addrlen);
    if (sfd < 0){
        // 阻塞等待,考虑被信号中断的情况(慢速系统调用,在系统阻塞期间有可能被信号中断)
        if ((errno == ECONNABORTED) || (errno == EINTR)){
            goto again;
        }else{
            print_err("accept error");
        }
    }
    return sfd;
}

int Listen(int sockfd, int backlog)
{
    int ret = listen(sockfd, backlog);
    if (ret < 0) print_err("listen error");
    return ret;
}

int Connect(int sockfd, const struct sockaddr_in *addr, socklen_t addrlen)
{
    int ret = connect(sockfd, (struct sockaddr *)addr, addrlen);
    if (ret < 0) print_err("connect error");
    return ret;
}

ssize_t Readn(int fd, void *buf, size_t count)
{
    size_t readed = 0;// 已读数
        size_t remain = count;// 未读数
    while(remain > 0){
        int n = read(fd, buf+readed, remain);
        if (n < 0){    // 异常处理
            if (errno == EINTR){
                n = 0;// 信号中断,继续循环
            }else{
                return n;// 异常情况直接返回
            }
        }else if (n < remain){
            remain -= n;
            break;
        }
        readed += n;
        remain -= n;
    }
    return count - remain;// 返回应读数-未读数
}

socket中读取数据read方法返回值分析
* > 0:实际读到的字节数 buf = 1024 或buf < 1024但 > 0;
* = 0:数据读完(读到文件、管道、socket 末尾—对端关闭);
* - 1:异常
* errno = = EINTR 被信号中断 重启/quit
* errno = = EAGAIN(EWOULDBLOCK)非阻塞方式读,并且没有数据
* errno = = 其他值,出现错误:perror( ) exit( );

7、TCP建立连接三次握手、关闭连接四次握手

  • SYN:代表请求创建连接,所以在三次握手中前两次要SYN=1,表示这两次用于建立连接,至于第三次什么用,在疑问三里解答。
  • FIN:表示请求关闭连接,在四次分手时,我们发现FIN发了两遍。这是因为TCP的连接是双向的,所以一次FIN只能关闭一个方向。
  • ACK:代表确认接受,从上面可以发现,不管是三次握手还是四次分手,在回应的时候都会加上ACK=1,表示消息接收到了,并且在建立连接以后的发送数据时,都需加上ACK=1,来表示数据接收成功。
  • seq:序列号,什么意思呢?当发送一个数据时,数据是被拆成多个数据包来发送,序列号就是对每个数据包进行编号,这样接受方才能对数据包进行再次拼接。初始序列号是随机生成的,这样不一样的数据拆包解包就不会连接错了。(例如:两个数据都被拆成1,2,3和一个数据是1,2,3一个是101,102,103,很明显后者不会连接错误)
  • ack:这个代表下一个数据包的编号,这也就是为什么第二请求时,ack是seq+1,

注意:ACK与ack

7.1、MTU&MSS

  • MTU:Maxitum Transmission Unit 最大传输单元
  • MSS:Maxitum Segment Size 最大分段大小
  • WIN:滑动窗口,当前本端能接收的数据上限值(单位:字节)
  1. MSS加包头数据就等于MTU. 简单说拿TCP包做例子。 报文传输1400字节的数据的话,那么MSS就是1400,再加上20字节IP包头,20字节tcp包头,那么MTU就是1400+20+20. 当然传输的时候其他的协议还要加些包头在前面,总之MTU就是总的最后发出去的报文大小。MSS就是你需要发出去的数据大小。

  2. 为了达到最佳的传输效能TCP协议在建立连接的时候通常要协商双方的MSS值,这个值TCP协议在实现的时候往往用MTU值代替(需要减去IP数据包包头的大小20Bytes和TCP数据段的包头20Bytes)所以往往MSS为1460。通讯双方会根据双方提供的MSS值得最小值确定为这次连接的最大MSS值。

Leave a Reply