C/C++服务器和客户端交互笔记

简介: Socket通信三要素:通信的目的地址、使用的端口号(http 80 / smtp 25)、使用的传输协议(TCP、UDP)。

C/C++服务器开发


网络与通信Socket


Socket通信三要素:通信的目的地址、使用的端口号(http 80 / smtp 25)、使用的传输协议(TCP、UDP)。


nslookup xx 可以查询xx网址的IP地址。


Socket通信模型


a0a9db7b908b41eaa3ac0ca81c2d10c8.png


telnet ipxx 进行主机间通信。


一个简单的服务器和客户端通信程序,服务器端代码:


#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <ctype.h>
#include <arpa/inet.h>
#define SERVER_PORT 666
int main(void)
{
    int sock;
    struct sockaddr_in server_addr;
    sock = socket(AF_INET, SOCK_STREAM, 0);
    // printf("wait \n");
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(SERVER_PORT);
    bind(sock, (struct sockaddr *)&server_addr, sizeof(server_addr));
    listen(sock, 128);
    printf("wait client connect\n");
    int done = 1;
    while (done)
    {
        struct sockaddr_in client;
        int client_sock, len, i;
        char client_ip[64];
        char buf[256];
        socklen_t client_addr_len;
        client_addr_len = sizeof(client);
        client_sock = accept(sock, (struct sockaddr *)&client, &client_addr_len);
        printf("client ip: %s \t port is : %d \n", inet_ntop(AF_INET, &client.sin_addr.s_addr, client_ip, sizeof(client_ip)), ntohs(client.sin_port));
        len = read(client_sock, buf, sizeof(buf) - 1);
        buf[len] = '\0';
        printf("receive[%d]:%s\n", len, buf);
        len = write(client_sock, buf, len);
        printf("finish. len:%d\n", len);
        close(client_sock);
    }
    close(sock);
    return 0;
}


客户端代码:


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define SERVER_PORT 666
#define SERVER_IP  "127.0.0.1"
int main(int argc, char *argv[]){
    int sockfd;
    char *message;
    struct sockaddr_in servaddr;
    int n;
    char buf[64];
    if(argc != 2){
        fputs("Usage: ./echo_client message \n", stderr);
        exit(1);
    }
    message = argv[1];
    printf("message: %s\n", message);
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
  //重置结构体的内存空间
    memset(&servaddr, '\0', sizeof(struct sockaddr_in));
    servaddr.sin_family = AF_INET;
    inet_pton(AF_INET, SERVER_IP, &servaddr.sin_addr);
    servaddr.sin_port = htons(SERVER_PORT);
    connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
    write(sockfd, message, strlen(message));
    n = read(sockfd, buf, sizeof(buf)-1);
    if(n>0){
        buf[n]='\0';
        printf("receive: %s\n", buf);
    }else {
        perror("error!!!");
    }
    printf("finished.\n");
    close(sockfd);
    return 0;
}


Socket概念


socket(套接字)的中文意思为插座,socket一般用整型表示,Linux中,表示进程x间网络通信的特殊文件类型。本质上为内核借助缓冲区形成的为文件。可以使用文件描述符引用套接字。Linux系统将其封装成文件的目的是为了统一接口,使读写套接字和读写文件操作一致。区别在于文件主要因用于本地持久化数据的读写,而套接字多应用于网络进程间数据的传递。


在TCP/IP协议中,IP地址-TCP或UDP端口号 唯一标识网络通讯中的一个进程。IP地址+端口号 就对应一个socket。与建立连接的两个进程各有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接。因此可以用Socket来描述网络连接的一对一关系。


eae22fab340047d9ac35c87da18726c9.png


网络通讯中,套接字一定是成对出现的。一段的发送缓冲区对应另一端的接收缓冲区。使用同一个文件描述符发送缓冲区和接收缓冲区。


5a975c1ec74e48ef8cc834a5d741426c.png


服务器和客户端之间的通讯是全双工的,可以互相读写,采用同步和异步的方式进行交互。


四次挥手结束客户端和服务器端的通讯。


网络字节序


大端字节序-低地址高字节,高地址低字节。

小端字节序-低地址低字节,高地址高字节。


内存中的多字节数据相对于内存地址、磁盘文件中的多字节数据相对于文件中的偏移地址,网络数据流都有大端和小端之分。发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节一次保存在接受缓冲区中,也是按照内存地址从低到高的顺序保存。因此,网络数据流的地址应该这样规定:先发出的数据是低地址,后发出的数据是高地址。


TCP/IP协议规定,网络数据流应采用大端字节序,既低地址高字节。


32位IP地址也要考虑网络字节序和主机字节序的问题。C/C++中采用一下库函数进行网络字节序和主机字节序的转换。


//头文件,库函数
#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);


h表示host,n表示network,l表示32位长整型,s表示16位短整型。


如果主机是小端字节序,这些函数将参数做相应的大小端转化然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。


SocketAddr详解


很多网络编程函数诞生早于IPv4协议,那时候都使用的是sockaddr结构体,为了向前兼容,现在sockaddr退化成了(void *)的作用,传递一个地址给函数,至于这个函数是sockaddr_in还是其他的,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。


82c0aed789cd48fa853d71efb93e6382.png


SocketAddress结构图


struct sockaddr {
  sa_family_t sa_family;/* address family, AF_xxx AF_INET(IPV4) AF_INET(IPV6)*/
  char sa_data[14];     /* 14 bytes of protocol address */
};
 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 */
};


IPv4的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位端口号和32位IP地址,但是sock API的实现早于ANSI C标准化,那时还没有void *类型,因此这些像bind 、accept函数的参数都用struct sockaddr *类型表示,在传递参数之前要强制类型转换一下,例如:


struct sockaddr_in servaddr;
bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));/* initialize servaddr */


IP地址转化


#include <arpa/inet.h>
//将字符串的IP转化为网络的整型IP
int inet_pton(int af, const char *src, void *dst);
//将网络字节序的IP转化为字符串的IP
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);


af 取值可选为 AF_INET 和 AF_INET6 ,即和 ipv4 和ipv6对应支持IPv4和IPv6假设主机地址位2.3.4.5,其中2表示低位,5表示高位,则大端字节序的结果为5040302(使用inet_pton(AF_INET,"2.3.4.5",&s_add)进行转化),小端字节序为2030405(使用ntohl(s_addr)进行转化)。


ipconfig /all查看主机的网络地址。


Socket编程


socket函数


//头文件
#include<sys/types.h>
#include<sys/socket.h>
int socket(int doamin,int type,int protocol);
domain:
  AF_INET 这是大多数用来产生socket的协议,使用TCP或UDP来传输,用IPV4的地址。
  AF_INET6 和AF_INET类似,不过是用来IPV6的地址。
  AF_UNIX 本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台主机及同时使用的的协议。
type:
  SOCK_STREAM 这个协议是按照顺序的、可靠的、数据完成的基于字节流的连接。这是一个使用最多的socket类型,用于TCP进行传输的。
  SOCK_DGRAM 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP来进行连接。
  SOCK_SEQPACKET 该协议是双线路的,可靠的链接,发送固定长度的数据包进行传输。必须把这个包完整接受才能进行读取。
  SOCK_RAW socket类型提供单一的网络访问,这个socket类型使用ICMP公共协议。(ping、traceroute使用该协议)
  SOCK_RDM 这个类型是很少使用的,在大部分操作系统上没有实现,它是提供给数据链路层使用,不保证数据包的顺序。
protocol:
  传0表示使用默认协议
return:
  成功:返回只想新创建的socket的文件描述符。失败:返回-1,并设置errno。


socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以向读写文件一样用read/write在网络上收发数据,如果socket()调用出错则返回-1。


bind函数


服务器将程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号就可以向服务器发起连接,因此服务器需要调用bind进行绑定。


//头文件
#include<sys/type.h>
#include<sys/socket.h>
int bind(int sockfd,const struct sockaddr *addr,socklen_t addren);
sockfd:
  socket文件描述符
addr:
  购找出IP地址加端口号
addrlen:
  sizeof(addr)长度
return:
  成功返回0。失败返回-1,设置errno。


bind()的作用是将参数sockfd和addr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听addr所描述的地址和端口号。struct sockaddr *是一个通用指针类型,addr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度。


struct sockaddr_in servaddr;
//结构体清空很重要
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(6666);


Listen函数


典型的服务器程序可以同时服务与多个客户端,当有客户端发起连接时,服务器调用的accept()函数返回并接受这个连接,如果有大量的可u段发起连接而服务器来不及处理,桑威accept的客户端就处于这个连接等待状态,listen()僧名sockfd处于监听状态,如果接受到更多的连接请求就忽略,listen()成功返回0,失败返回-1.


//头文件
#include<sys/types.h>
#include<sys/socket.h>
int listen(int sockfd,int backlog);
sockfd:
  socket文件描述符
backlog:
  在Linux系统中,它是指排队等待建立3次握手队列长度


查看系统默认backlog


cat /proc/sys/net/ipv4/tcp_max_syn_backlog


accept函数


//头文件
#include<sys/types.h>
#include<sys/socket.h>
int accept(int sockfd,struct aockaddr *addr,socklen_t *addrlen);
sockdf:
  socket文件描述符
addr:
  传出的参数,返回链接客户端地址信息,含IP地址和端口号
addrlen:
  传入传出参数(值-结果),传入sizeof(addr)大小,函数返回时返回真正接受到地址结构体的大小
return:
  返回一个新的socket文件描述符,用于和客户端通信,失败返回-1,并设置errno


三次握手以后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。addr是一个传出参数,accept()返回时传出客户端的地址和端口号。


服务器端代码结构案例:


while (1) {
  cliaddr_len = sizeof(cliaddr);
  //如果没有客户端连接就会一直堵塞在这个代码上,不会往下进行执行
  connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
  n = read(connfd, buf, MAXLINE);
  .......
  close(connfd);
}


整个结构是一个while死循环,每次循环处理一个客户端连接,由于cliaddr_len是一个传入传出参数,每次调用accept()之前应该重新赋初值。accept()的参数listenfs是先前监听的文件描述符,而accept()的返回值是另外一个文件描述符connfd,之后与客户端之间就是通过或者connfd通讯,最后关闭connfd断开连接,而不关闭listenfd,再次回到循环开头listenfd仍然用作accept参数。accept()成功返回一个文件描述符,出错返回-1。


connect函数


//头文件
#include<sys/types.h>
#include<sys/socket.h>
int connect(int sockfd,const struct sockaddr *addr,socklen_t addrlen);
sockdf:
  socket文件描述符
addr:
  传入参数,指定服务器端地址信息,含IP地址和端口号
addrlen:
  传入参数,指定服务器段地址信息,含IP地址和端口信息
return:
  成功返回0,失败返回-1,并设置errno


客户端需要调用connect()连接服务器,connect和bind的参数一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址。


出错处理函数


系统调用不能保证每次执行都成功,应该尽快获得程序故障信息。


//头文件
#include<errno.h>
#include<string.h>
char *strerror(int errnum)
errnum:
  传入参数,错误编号的值,一般去errno的值
return:
  错误原因
#include<stdio.h>
#include<errno.h>
void perror(const char *s);
s:
  传入参数,自定义描述
return:
向标准出错stdeer输出出错原因(控制台打印)。


目录
相关文章
|
1月前
|
Python
Socket学习笔记(二):python通过socket实现客户端到服务器端的图片传输
使用Python的socket库实现客户端到服务器端的图片传输,包括客户端和服务器端的代码实现,以及传输结果的展示。
130 3
Socket学习笔记(二):python通过socket实现客户端到服务器端的图片传输
|
1月前
|
JSON 数据格式 Python
Socket学习笔记(一):python通过socket实现客户端到服务器端的文件传输
本文介绍了如何使用Python的socket模块实现客户端到服务器端的文件传输,包括客户端发送文件信息和内容,服务器端接收并保存文件的完整过程。
135 1
Socket学习笔记(一):python通过socket实现客户端到服务器端的文件传输
|
1月前
使用Netty实现文件传输的HTTP服务器和客户端
本文通过详细的代码示例,展示了如何使用Netty框架实现一个文件传输的HTTP服务器和客户端,包括服务端的文件处理和客户端的文件请求与接收。
38 1
使用Netty实现文件传输的HTTP服务器和客户端
|
30天前
|
存储 弹性计算 算法
前端大模型应用笔记(四):如何在资源受限例如1核和1G内存的端侧或ECS上运行一个合适的向量存储库及如何优化
本文探讨了在资源受限的嵌入式设备(如1核处理器和1GB内存)上实现高效向量存储和检索的方法,旨在支持端侧大模型应用。文章分析了Annoy、HNSWLib、NMSLib、FLANN、VP-Trees和Lshbox等向量存储库的特点与适用场景,推荐Annoy作为多数情况下的首选方案,并提出了数据预处理、索引优化、查询优化等策略以提升性能。通过这些方法,即使在资源受限的环境中也能实现高效的向量检索。
|
1月前
|
网络协议 Unix Linux
一个.NET开源、快速、低延迟的异步套接字服务器和客户端库
一个.NET开源、快速、低延迟的异步套接字服务器和客户端库
|
1月前
|
网络协议 Java API
【网络】TCP回显服务器和客户端的构造,以及相关bug解决方法
【网络】TCP回显服务器和客户端的构造,以及相关bug解决方法
61 2
|
1月前
|
存储 网络协议 Java
【网络】UDP回显服务器和客户端的构造,以及连接流程
【网络】UDP回显服务器和客户端的构造,以及连接流程
52 2
|
1月前
|
Linux C语言 C++
vsCode远程执行c和c++代码并操控linux服务器完整教程
这篇文章提供了一个完整的教程,介绍如何在Visual Studio Code中配置和使用插件来远程执行C和C++代码,并操控Linux服务器,包括安装VSCode、安装插件、配置插件、配置编译工具、升级glibc和编写代码进行调试的步骤。
198 0
vsCode远程执行c和c++代码并操控linux服务器完整教程
|
30天前
|
安全 区块链 数据库
|
1月前
|
存储 网络协议 Unix
docker的底层原理一:客户端-服务器架构
本文详细解释了Docker的客户端-服务器架构,包括常驻后台的Docker守护进程、通过命令行接口发送请求的Docker客户端、以及它们之间通过Unix socket或网络接口进行的通信。
20 0