• socket网络编程


    tcp协议  (可靠,一般用于文件传输)  

    tcp 协议特点:面向连接的,可靠的(相对于udp协议),流式服务

    udp协议  (不可靠,但是实时性好)

    一、tcp 协议

    TCP 编程流程

    TCP 提供的是面向连接的、可靠的、字节流服务。

    TCP 的服务器端和客户端编程流程如 下:

    二、主机字节序列和网络字节序列

    规定使用大端字节序列作为网络字节序列

    主机字节序列分为大端字节序和小端字节序,不同的主机采用的字节序列可能不同。

    大端字节序是指一个整数的高位字节存储在内存的低地址处,低位字节存储在内存的高地址处。 

    小端字节序则是指整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的低地址处。

    在两台使用不同字节序的主机之间传递数据时,可能会出现冲突。所以,在将数据发送到网络时规定整形数据使用大端字节序,所以也把大端字节序成为网络字节序列。对方接收到数据后,可以根据自己的字节序进行转换。

    Linux 系统提供如下 4 个函数来完成主机字节序和网络字节序之间的转换

    #include

    uint32_t htonl(uint32_t hostlong); // 长整型的主机字节序转网络字节序

    uint32_t ntohl(uint32_t netlong); // 长整型的网络字节序转主机字节序

    uint16_t htons(uint16_t hostshort); // 短整形的主机字节序转网络字节序

    uint16_t ntohs(uint16_t netshort); // 短整型的网络字节序转主机字节序

    三、套接字地址结构

    套接字地址——ip+端口

    3.1 通用 socket 地址结构

    socket 网络编程接口中表示 socket 地址的是结构体 sockaddr,其定义如下:

    1. #include
    2. struct sockaddr
    3. {
    4. sa_family_t sa_family;
    5. char sa_data[14];
    6. };

    sa_family 成员是地址族类型(sa_family_t)的变量。地址族类型通常与协议族类型对 应。常见的协议族和对应的地址族如下图所示:

     3.2 专用 socket 地址结构

    TCP/IP 协议族有 sockaddr_in 和 sockaddr_in6 两个专用 socket 地址结构体,

    它们分别用于 IPV4 和 IPV6:

    sin_family:地址族 AF_INET 

    sin_port::端口号,需要用网络字节序表示

    sin_addr: IPV4

    地址结构:s_addr 以网络字节序表示 IPV4 地址

    1. struct in_addr
    2. {
    3. u_int32_t s_addr;
    4. };
    5. struct sockaddr_in//IPV4专用的,sockaddr 通用的
    6. {
    7. sa_family_t sin_family;
    8. u_int16_t sin_port;
    9. struct in_addr sin_addr;
    10. };
    11. struct in6_addr
    12. {
    13. unsigned char sa_addr[16]; // IPV6 地址,要用网络字节序表示
    14. };
    15. struct sockaddr_in6
    16. {
    17. sa_family_t sin6_family; // 地址族:AF_INET6
    18. u_inet16_t sin6_port; // 端口号:用网络字节序表示
    19. u_int32_t sin6_flowinfo; // 流信息,应设置为 0
    20. struct in6_addr sin6_addr; // IPV6 地址结构体
    21. u_int32_t sin6_scope_id; // scope ID,尚处于试验阶段
    22. };

    设计定义定义时,socket 地址为通用的,传的时候可以是专有的

    3.3 IP 地址转换函数——点分十进制与整型互转

    通常,人们习惯用点分十进制字符串表示 IPV4 地址,但编程中我们需要先把它们转化为整数方能使用,下面函数可用于点分十进制字符串表示的 IPV4 地址和网络字节序整数表示的 IPV4 地址之间的转换:

    1. #include
    2. in_addr_t inet_addr(const char *cp); //字符串表示的 IPV4 地址转化为网络字节序
    3. char* inet_ntoa(struct in_addr in); // IPV4 地址的网络字节序转化为字符串表示

    struct in_addr 里面是一个无符号整型

     四、网络编程接口

    1. #include
    2. #include

     

    4.1 创建套接字

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

    socket()  创建套接字,成功返回套接字的文件描述符,失败返回-1

    domain: 设置套接字的协议簇,

                     AF_UNIX      AF_INET (刚开始大多使用这个,IPV4)AF_INET6(IPV6)

    type: 设置套接字的服务类型 SOCK_STREAM(TCP协议选择流式服务) SOCK_DGRAM

    protocol:一般设置为 0,表示使用默认协议

    有了套接字就可以通过网络进行数据的收发。进行网络通信的程序首先就要创建一个套接字。

    4.2 命名,指定套接字的ip地址和端口

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

    bind()    将 sockfd 与一个 socket 地址绑定,成功返回 0,失败返回-1

    sockfd  是网络套接字描述符

    addr     是地址结构(见3.2),服务器的地址,客户端链接这个地址 struct sockaddr通用的

    addrlen 是 socket 地址的长度

    IP 地址就是自己主机的地址,如果主机没有接入网络,测试程序时可以使用回环地址“127.0.0.1”。端口是一个 16 位的整形值, 一般 0-1024 为知名端口,如 HTTP 使用的 80 号端口。这类端口一般用户不能随便使用。其 次,1024-4096 为保留端口,用户一般也不使用。4096 以上为临时端口,用户可以使用。在 Linux 上,1024 以内的端口号,只有 root 用户可以使用。

    4.3 监听

    int listen(int sockfd, int backlog);//这条语句一定不会阻塞,这只是代表创建一个位置,以便存放链接

    listen()      创建一个监听队列存储待处理的客户连接,成功返回 0,失败返回-1

    sockfd      是被监听的 socket 套接字

    backlog    表示处于完全连接状态的 socket 的上限

    监听队列有两种,一个是存放未完成三次握手的连接, 一种是存放已完成三次握手的连接。listen()第二个参数就是指定已完成三次握手队列的长度。

    4.4 接收(客户端的链接)

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

    accept()   从 listen 监听队列中接收一个连接,成功返回一个新的连接 socket

                    该 socket 唯一地标识了被接收的这个连接,失败返回-1

    sockfd     是执行过 listen 系统调用的监听 socket

    addr        参数用来获取被接受连接的远端 socket 地址(客户端地址)

    addrlen   指定该 socket 地址的长度

    accept()处理存放在 listen 创建的已完成三次握手的队列中的连接。每处理一个连接,则 accept()返回该连接对应的套接字描述符。如果该队列为空,则 accept 阻塞。

    4.5 发起链接(客户端发起)

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

    connect()    客户端需要通过此系统调用来主动与服务器建立连接,成功返回 0,失败返回-1 sockfd         参数是由 socket()返回的一个 socket

    serv_addr   是服务器监听的 socket 地址,服务器的地址,服务器使用这个地址

    addrlen       则指定这个地址的长度

    connect()方法一般由客户端程序执行,需要指定连接的服务器端的 IP 地址和端口。该方 法执行后,会进行三次握手, 建立连接。

    4.6 关闭

    int close(int sockfd);

    close()   关闭一个连接,实际上就是关闭该连接对应的 socket

    close()方法用来关闭 TCP 连接。此时,会进行四次挥手。

    4.7 TCP数据读写(收发数据)

    1. ssize_t recv(int sockfd, void *buff, size_t len, int flags);
    2. ssize_t send(int sockfd, const void *buff, size_t len, int flags);

    recv()    读取 sockfd 上的数据,buff 和 len 参数分别指定读缓冲区的位置和大小

    send()   往 socket 上写入数据,buff 和 len 参数分别指定写缓冲区的位置和数据长度

    flags      参数为数据收发提供了额外的控制

    send()方法用来向 TCP 连接的对端发送数据。send()执行成功,只能说明将数据成功写入到发送端的发送缓冲区中,并不能说明数据已经发送到了对端。send()的返回值为实际写入到发送缓冲区中的数据长度。

    recv()方法用来接收 TCP 连接的对端发送来的数据。recv()从本端的接收缓冲区中读取数据,如果接收缓冲区中没有数据,则 recv()方法会阻塞。返回值是实际读到的字节数,如果 recv()返回值为 0, 说明对方已经关闭了 TCP 连接。

    4.8 UDP数据读写

    1. ssize_t recvfrom(int sockfd, void *buff, size_t len, int flags,struct sockaddr* src_addr, socklen_t *addrlen);
    2. ssize_t sendto(int sockfd, void *buff, size_t len, int flags,struct sockaddr* dest_addr, socklen_t addrlen);

    recvfrom()     读取 sockfd 上的数据,buff 和 len 参数分别指定读缓冲区的位置和大小  src_addr       记录发送端的 socket 地址

    addrlen         指定该地址的长度

    sendto()        往 socket 上写入数据,buff 和 len 参数分别指定写缓冲区的位置和数据长度 dest_addr     指定接收数据端的 socket 地址

    addrlen         指定该地址的长度

    五、TCP 服务器客户端代码实现

    Linux中使用 ifconfig 查看ip

    windows中使用 ipconfig 查看ip

    一个客户端访问服务器期间不能被其他客户端访问

    服务器端

    ser.c

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. int main()
    9. {
    10. int sockfd=socket(AF_INET,SOCK_STREAM,0);//创建套接字
    11. if(sockfd == -1)
    12. {
    13. printf("create socket err\n");
    14. exit(1);
    15. }
    16. //IPV4
    17. //“127.0.0.1”回环地址,测试地址,谁写代表谁
    18. struct sockaddr_in saddr,caddr;//ipv4专用地址 saddr 服务器地址 caddr 客户端地址
    19. memset(&saddr,0,sizeof(saddr));//将地址空间内容清零,要先清空后续再填充
    20. saddr.sin_family=AF_INET;//地址族
    21. saddr.sin_port=htons(6000);//6000 端口号,4096以上临时端口可以使用,htons 转为网络字节序列
    22. saddr.sin_addr.s_addr=inet_addr("0.0.0.0");//将字符串转为无符号整型才可以使用 IPV4地址 “0.0.0.0”系统会自动识别ip地址,也可以直接给ip地址,若单纯只是测试,也可以使用回环地址“127.0.0.1”
    23. //命名,将sockfd与一个socket地址绑定
    24. int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//(struct sockaddr*)saddr 将IPV4专用地址改为通用地址
    25. if(res==-1)
    26. {
    27. printf("bind err\n");//失败的原因:端口被占用,地址错误....
    28. exit(1);
    29. }
    30. //创建监听队列
    31. res = listen(sockfd,5);
    32. if(res==-1)
    33. {
    34. printf("listen err\n");
    35. exit(1);
    36. }
    37. //接收(客户端连接)
    38. while(1)
    39. {
    40. int len=sizeof(caddr);//客户端地址长度
    41. int c = accept(sockfd,(struct sockaddr*)&caddr,&len);//如果没有传入客户端地址,就会阻塞 成功返回一个新的链接 每个客户端过来连接服务器都会分配一个c
    42. if(c<0)
    43. {
    44. continue;
    45. }
    46. printf("ip:%s,port=%d\n",inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port));//ip 端口
    47. char buff[128]={0};
    48. int n = recv(c,buff,127,0);//接收数据,返回值为实际读到的字节数
    49. printf("recv(c=%d,n=%d)=%s\n",c,n,buff);
    50. send(c,"OK",2,0);//发送数据
    51. close(c);//关闭客户端连接
    52. }
    53. }

    客户端

    cli.c

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. int main()
    9. {
    10. //创建套接字
    11. int sockfd = socket(AF_INET,SOCK_STREAM,0);
    12. if(sockfd==-1)
    13. {
    14. printf("socket err\n");
    15. exit(1);
    16. }
    17. //发起连接,客户端主动发起连接,需要知道服务器的地址,就可以连接成功,就像打电话知道对方手机号码就可以成功,不局限于是哪个号码打的
    18. //客户端端口号系统随机分配
    19. struct sockaddr_in saddr;//服务器地址
    20. memset(&saddr,0,sizeof(saddr));
    21. saddr.sin_family=AF_INET;
    22. saddr.sin_port=htons(6000);
    23. saddr.sin_addr.s_addr=inet_addr("192.168.5.5");//这里的地址应该是服务器地址,
    24. int res=connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//连接ip上的端口
    25. if(res==-1)
    26. {
    27. printf("connect err\n");
    28. exit(1);
    29. }
    30. //发送数据
    31. char buff[128]={0};
    32. printf("input:\n");
    33. fgets(buff,128,stdin);
    34. send(sockfd,buff,strlen(buff)-1,0);
    35. //接收反馈信息
    36. memset(buff,0,128);//清空buff中的数据,用来接收服务器发送的信息
    37. recv(sockfd,&buff,127,0);
    38. printf("buff=%s\n",buff);
    39. //关闭连接
    40. close(sockfd);
    41. }

    要先运行服务器端,再运行客户端

    线程实现多台客户机同时连接服务器

    多线程并发处理实现多台客户机同时连接服务器

    在连接的时候产生一个线程 ,让线程去处理客户端

    三次握手在传输层

    三次握手用来建立连接

     connect()方法一般由客户端程序执行,需要指定连接的服务器端的 IP 地址和端口。该方 法执行后,会进行三次握手, 建立连接。

    服务器端

    ser.c

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. int socket_init();
    10. void* fun();
    11. int main()
    12. {
    13. int sockfd=socket_init();
    14. if(sockfd==-1)
    15. {
    16. exit(1);
    17. }
    18. while(1)
    19. {
    20. pthread_t id;
    21. struct sockaddr_in caddr;
    22. int len=sizeof(caddr);
    23. int c=accept(sockfd,(struct sockaddr*)&caddr,&len);
    24. if(c<0)
    25. {
    26. printf("accept err\n");
    27. exit(1);
    28. }
    29. printf("c=%d,ip:%s,port=%d\n",c,inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port));
    30. pthread_create(&id,NULL,fun,(void*)c);
    31. }
    32. }
    33. void* fun(void* arg)//循环收发数据
    34. {
    35. int c = (int)arg;
    36. while(1)
    37. {
    38. char buff[128]={0};
    39. int n=recv(c,&buff,127,0);
    40. if(n<=0)//=0表示连接关闭
    41. {
    42. break;
    43. }
    44. printf("recv(c=%d)=%s\n",c,buff);
    45. send(c,"OK",2,0);
    46. }
    47. printf("close\n");
    48. close(c);
    49. }
    50. int socket_init()
    51. {
    52. int sockfd=socket(AF_INET,SOCK_STREAM,0);//创建套接字
    53. if(sockfd==-1)
    54. {
    55. printf("socket err\n");
    56. exit(1);
    57. }
    58. struct sockaddr_in saddr;
    59. memset(&saddr,0,sizeof(saddr));//必须清零
    60. saddr.sin_family=AF_INET;
    61. saddr.sin_port=htons(6000);
    62. saddr.sin_addr.s_addr=inet_addr("0.0.0.0");
    63. int ser=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//将IPV4专用的地址强转为通用的
    64. if(ser==-1)
    65. {
    66. printf("bind err\n");
    67. exit(1);
    68. }
    69. ser=listen(sockfd,5);
    70. if(ser==-1)
    71. {
    72. printf("listen err\n");
    73. exit(1);
    74. }
    75. return sockfd;
    76. }

    客户端

    cli.c

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. int main()
    10. {
    11. int sockfd=socket(AF_INET,SOCK_STREAM,0);
    12. if(sockfd==-1)
    13. {
    14. printf("socket err\n");
    15. exit(1);
    16. }
    17. struct sockaddr_in saddr;//服务器地址
    18. memset(&saddr,0,sizeof(saddr));
    19. saddr.sin_port=htons(6000);
    20. saddr.sin_family=AF_INET;
    21. saddr.sin_addr.s_addr=inet_addr("192.168.43.71");
    22. //开始三次握手,connect返回成功,则三次握手完成
    23. int res = connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
    24. if(res==-1)
    25. {
    26. printf("connect err\n");
    27. exit(1);
    28. }
    29. while(1)
    30. {
    31. char buff[128]={0};
    32. printf("input:\n");
    33. fgets(buff,128,stdin);
    34. if(strncmp(buff,"end",3)==0)
    35. {
    36. break;
    37. }
    38. send(sockfd,buff,strlen(buff)-1,0);
    39. //接收反馈信息
    40. memset(buff,0,128);//清空buff中的数据,用来接收服务器发送的信息
    41. recv(sockfd,&buff,127,0);
    42. printf("buff=%s\n",buff);
    43. }
    44. close(sockfd);
    45. }

    命令:netstat -natp

    服务器每次只接收一个字符

    1. void* fun(void* arg)//循环收发数据
    2. {
    3. int c = (int)arg;
    4. while(1)
    5. {
    6. char buff[128]={0};
    7. int n=recv(c,&buff,1,0);//每次只接收一个字符
    8. if(n<=0)//=0表示连接关闭
    9. {
    10. break;
    11. }
    12. printf("recv(c=%d)=%s\n",c,buff);
    13. send(c,"OK",2,0);
    14. }
    15. printf("close\n");
    16. close(c);
    17. }

    查看缓冲区看是否有数据

     说明剩下的的4个OK都发送过来了,在客户端的缓冲区中,只是没有机会读取出来

    但是我们客户端读取数据的时候可以读取127个字符,为什么只读到了2个呢?

     程序运行结果说明当时客户端读缓冲区时只有两个字符,剩下的(4个OK)8个字符还没过来,所以recv就只将有的读走了,接着程序就运行到input等待输入字符,没有机会读取缓冲区

     查看缓冲区

     进程实现多态客户机同时连接服务器

    服务器

    ser.c

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include
    10. int socket_init();
    11. void fun();
    12. int main()
    13. {
    14. int sockfd=socket_init();
    15. if(sockfd==-1)
    16. {
    17. exit(1);
    18. }
    19. while(1)
    20. {
    21. pid_t id=fork();
    22. struct sockaddr_in caddr;
    23. int len=sizeof(caddr);
    24. int c=accept(sockfd,(struct sockaddr*)&caddr,&len);
    25. if(c<0)
    26. {
    27. printf("accept err\n");
    28. exit(1);
    29. }
    30. printf("c=%d,ip:%s,port=%d\n",c,inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port));
    31. fun(&c);
    32. }
    33. }
    34. void fun(int *arg)//循环收发数据
    35. {
    36. int c=*arg;
    37. while(1)
    38. {
    39. char buff[128]={0};
    40. int n=recv(c,&buff,127,0);//每次只接收一个字符
    41. if(n<=0)//=0表示连接关闭
    42. {
    43. break;
    44. }
    45. printf("recv(c=%d)=%s\n",c,buff);
    46. send(c,"OK",2,0);
    47. }
    48. printf("close\n");
    49. close(c);
    50. }
    51. int socket_init()
    52. {
    53. int sockfd=socket(AF_INET,SOCK_STREAM,0);//创建套接字
    54. if(sockfd==-1)
    55. {
    56. printf("socket err\n");
    57. exit(1);
    58. }
    59. struct sockaddr_in saddr;
    60. memset(&saddr,0,sizeof(saddr));//必须清零
    61. saddr.sin_family=AF_INET;
    62. saddr.sin_port=htons(6000);
    63. saddr.sin_addr.s_addr=inet_addr("0.0.0.0");
    64. int ser=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//将IPV4专用的地址强转为通用的
    65. if(ser==-1)
    66. {
    67. printf("bind err\n");
    68. exit(1);
    69. }
    70. ser=listen(sockfd,5);
    71. if(ser==-1)
    72. {
    73. printf("listen err\n");
    74. exit(1);
    75. }
    76. return sockfd;
    77. }

  • 相关阅读:
    红光光浴-改善亚健康状态
    高级架构师_Docker_第2章_ Docker核心原理_ 第7节IDEA集成Docker
    【CC3200AI 实验教程 1】疯壳·AI语音人脸识别(会议记录仪/人脸打卡机)-开发环境搭建
    rosbag 详细使用
    位移贴图和法线贴图的区别
    【微信小程序】button和image组件的基本使用
    DLP投影仪工作原理
    Jenkins安装多个jdk版本,并在项目中选择对应jdk版本
    【递归算法】输入任意一种物质的化学分子式,要求输出其每种元素的数量.
    设计模式-行为型-中介者模式
  • 原文地址:https://blog.csdn.net/swint_er/article/details/126142417