Socket编程:基于TCP协议的客户端与服务端网络通信

1. 服务端创建流程

  1. 调用socket函数创建监听socket;
  2. 调用bind函数将socket绑定到某个IP和端口号组成的二元组上;
  3. 调用listen函数开启监听;
  4. 当有客户端连接请求时,调用accept函数接受连接,产生一个新的socket(与客户端通信的socket);
  5. 基于新产生的socket调用send或recv函数开始与客户端进行数据交流;
  6. 通信结束后,调用close函数关闭socket。

2. 客户端创建流程

  1. 调用socket函数创建客户端socket;
  2. 调用connect函数尝试连接服务器;
  3. 连接成功后调用send或recv函数与服务器进行数据交流。
  4. 通信结束后,调用close函数关闭socket。

3. 常用API

struct sockaddr 和 struct sockaddr_in

这两个结构体都是用来处理网络通信的地址

/*
 *此数据结构用做bind、connect、recvfrom、sendto等函数的参数,指明地址信息
 *note:
 *  目标地址和端口信息在一起
 */
#include <sys/socket.h>

struct sockaddr
{
    
    
    // 地址家族,一般“AF_xxx”的形式,通常使用AF_INET
    unsigned short sa_family;
    // 14字节协议地址,目标地址和端口信息
    char           sa_data[14];
}
#include <netinet/in.h>

struct  sockaddr_in 
{
    
    
    short int       sin_family;       //协议族
    unsigned short  int  sin_port;    //端口号(使用网络字节顺序)
    struct in_addr  sin_addr;         //IP地址        
    unsigned char   sin_zero[8];      //sockaddr与sockaddr_in 保持大小相同而保留的空字节
};

struct  in_addr 
{
    
    
    unsigned  long  s_addr;
};

typedef struct in_addr 
{
    
    
    union
    {
    
    
        struct
        {
    
    
            unsigned char s_b1,
                          s_b2,
                          s_b3,
                          s_b4;
        } S_un_b;

        struct 
        {
    
    
            unsigned short s_w1,
                           s_w2;
        } S_un_w;

        unsigned long S_addr;
    } S_un;
} IN_ADDR;

sockaddr_insockaddr并列的结构,指向sockaddr_in的结构体的指针,同样可以指向sockraddr的结构体,并代替它。

struct sockaddr_in mysock;

bind(sockfd, (struct sockaddr *)&mysock, sizeof(struct sockaddr); /* bind的时候进行转化 */

net.core.somaxconn

net.core.somaxconn是Linux中的一个kernel参数,表示socket监听(listen)的backlog上限。backlog是socket的监听队列,当一个请求(request)尚未被处理或建立时,他会进入backlog。而socket server可以一次性处理backlog中的所有请求,处理后的请求不再位于监听队列中。当server处理请求较慢,以至于监听队列被填满后,新来的请求会被拒绝。

在Hadoop 1.0中,参数ipc.server.listen.queue.size控制了服务端socket的监听队列长度,即backlog长度,默认值是128。而Linux的参数net.core.somaxconn默认值同样为128。当服务端繁忙时,如NameNode或JobTracker,128是远远不够的。这样就需要增大backlog,例如我们的3000台集群就将ipc.server.listen.queue.size设成了32768,为了使得整个参数达到预期效果,同样需要将kernel参数net.core.somaxconn设成一个大于等于32768的值。

Linux中可以工具syctl来动态调整所有的kernel参数。所谓动态调整就是kernel参数值修改后即时生效。但是这个生效仅限于os层面,对于Hadoop来说,必须重启应用才能生效。

显示所有的kernel参数及值

sysctl -a

修改参数值的语法

sysctl -w net.core.somaxconn=32768

以上命令将kernel参数net.core.somaxconn的值改成了32768。这样的改动虽然可以立即生效,但是重启机器后会恢复默认值。为了永久保留改动,需要用vi在/etc/sysctl.conf中增加一行

net.core.somaxconn=4000

然后执行命令

sysctl -p

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

  • 作用:创建一个套接字描述符。在Linux系统中,一切皆文件。为了表示和区分已经打开的文件,Unix/Linux会给文件分配一个ID,这个ID是一个整数,被称为文件描述符。因此,网络连接也是一个文件,它也有文件描述符。通过socket()函数来创建一个网络连接或者说打开一个网络文件,socket()函数的返回值就是文件描述符,通过这个文件描述符我们就可以使用普通的文件操作来传输数据了。

  • 参数:

    family:指明了协议族/域,通常AF_INET、AF_INET6、AF_LOCAL等;
    type:套接字类型,主要 SOCK_STREAM、SOCK_DGRAM、SOCK_RAW等;
    protocol:一般取为0。

  • 返回值:ok(非负),error(-1)。成功时,返回一个小的非负整数值,与文件描述符类似。

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

  • 作用:服务端把用于通信的地址和端口绑定到socket上。

  • 参数:

    sockfd:代表需要绑定的socket,即在创建socket套接字时返回的文件描述符;
    addr:存放了服务端用于通信的地址和端口;
    addrlen:代表addr结构体的大小。

  • 返回值:当bind函数返回0时,为正确绑定;返回-1,则为绑定失败。

int listen(int sockfd, int backlog);

  • 作用:listen函数的功能并不是等待一个新的connect的到来,真正等待connect的是accept函数。listen的操作就是当有较多的client发起connect时,server端不能及时处理连接请求,这时就会将connect连接放在等待队列中缓存起来。这个等待队列的长度由listen中的backlog参数来设定。

  • 参数:

    sockfd:前面socket创建的文件描述符;
    backlog:指server端可以缓存连接的最大个数,也就是等待队列的长度。

  • 返回值:当listen运行成功时,返回0;运行失败时,返回-1。

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

  • 作用:accept函数等待客户端的连接,如果没有客户端连上来,它就一直等待,这种方式称为阻塞。accept等待到客户端的连接后,创建一个新的socket,函数返回值就是这个新的socket,服务端用这个新的socket和客户端进行消息的收发。

  • 参数:

    sockfd:已经被listen过的socket;
    client_addr:用于存放客户端的地址信息,其中包含客户端的协议族,网络地址以及端口号。如果不需要客户端的地址,可以填0;
    addrlen:用于存放参数二(client_addr)的长度。

  • 返回值:ok(新的 fd ),error(-1)。

int connect(int sock_fd, struct sockaddr *serv_addr, int addrlen);

  • 作用:客户端向服务端发起连接请求。

  • 参数:

    sock_fd:代表通过socket()函数返回的文件描述符;
    serv_addr:代表目标服务器的协议族,网络地址以及端口号,是一个sockaddr 类型的指针;
    addrlen:代表第二个参数内容的大小。

  • 返回值:当返回值是0时,代表连接成功;返回值为-1时,代表连接失败。

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

  • 作用:recv函数用于接收对端socket发送过来的数据。不论是客户端还是服务端,应用程序都用recv函数接受来自TCP连接的另一端发送过来的数据。如果socket对端没有发送数据,recv函数就会等待,如果对端发送了数据,函数返回接收到的字符数。

  • 参数:

    sockfd:代表接收端的套接字描述符,即通过socket()函数返回的文件描述符;
    buf:用于接收数据的内存地址,可以是C语言基本数据类型变量的地址,也可以是数组、结构体、字符串;
    len:指明需要接收数据的字节数。不能超过buf的大小,否则内存溢出;
    flags:一般设置为0,其他数值意义不大。

  • 返回值:失败时,返回值小于0;超时或对端主动关闭,返回值等于0;成功时,返回值是接收数据的长度。

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

  • 作用:send函数用于把数据通过socket发送给对端。不论是客户端还是服务端,应用程序都用send函数来向TCP连接的另一端发送数据。

  • 参数:

    sockfd:代表发送端的套接字描述符,即通过socket()函数返回的文件描述符;
    buf:指明需要发送数据的内存地址,可以是C语言基本数据类型变量的地址,也可以是数组、结构体、字符串;
    len:指明实际发送数据的字节数;
    flags:一般设置为0,其他数值意义不大。

  • 返回值:失败时,返回值小于0;超时或对端主动关闭,返回值等于0;成功时,返回值是发送数据的长度。

int close(int sockfd);

  • 作用:关闭套接字,并终止TCP连接。

  • 参数:

    sockfd:套接字描述符。

  • 返回值:ok(0),error(-1)。

int inet_pton(int af, const char *src, void *dst);

  • 作用:将文本地址转化为二进制地址。

  • 头文件:#include <arpa/inet.h>

  • 参数:

    af:地址族。AF_INET:IPv4 地址;AF_INET6:IPv6 地址;
    src:指向“点分式”IPv4 或 IPv6 地址的指针,例如“192.168.1.100”;
    dst:类型为 struct in_addr *或者 struct in6_addr *的指针。

  • 返回值:成功:1;失败:0 代表地址与地址族不匹配,-1 代表地址不合法。

const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

  • 作用:将二进制地址转化为文本地址。

  • 头文件:#include <arpa/inet.h>

  • 参数:

    af:地址族。AF_INET:IPv4 地址;AF_INET6:IPv6 地址;
    src:类型为 struct in_addr *或者 struct in6_addr *的指针;
    dst:地址缓冲区指针;
    size:地址缓冲区大小,至少要 INET_ADDRSTRLEN 或者 INET6_ADDRSTRLEN 个字节。

  • 返回值:成功:dst; 失败:NULL。

字节序的转换函数

  • 函数原型:

    uint32_t htonl(uint32_t hostlong);
    uint16_t htons(uint16_t hostshort);
    uint32_t ntohl(uint32_t netlong);
    uint16_t ntohs(uint16_t netshort);

  • 参数:
    hostlong:主机字节序的长整型数据;
    hostshort:主机字节序的短整型数据;
    netlong: 网络字节序的长整型数据;
    netshort:网络字节序的短整型数据。

  • 返回值:对应的字节序数据。

4. 服务端代码

#include <iostream>
#include <unistd.h>		// POSIX系统API访问
#include <sys/types.h>	// 基本系统数据类型
#include <arpa/inet.h>	// 网络信息转换
#include <string.h>

using namespace std;

#define SERVER_PORT 9991

int main() {
    
    
    // 创建一个监听socket
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);	

    if (listenfd == -1) {
    
    
        cout << "create listen socket error!" << endl;
        return -1;
    }

    // 初始化服务器地址
    struct sockaddr_in bindaddr;
    bindaddr.sin_family = AF_INET;
    bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    bindaddr.sin_port = htons(SERVER_PORT);
	//如果只想在本机上进行访问,bind函数地址可以使用本地回环地址
	//如果只想被局域网的内部机器访问,那么bind函数地址可以使用局域网地址
	//如果希望被公网访问,那么bind函数地址可以使用INADDR_ANY or 0.0.0.0

    // 绑定地址和端口
    if (bind(listenfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr)) == -1) {
    
    
        cout << "bind listen socket error!" << endl;
        return -1;
    }

    // 启动监听
    if (listen(listenfd, SOMAXCONN) == -1) {
    
    
        cout << "listen error!" << endl;
        return -1;
    }
	cout << "start listening..." << endl;

    while (true) {
    
    
        // 创建一个临时的客户端socket
        struct sockaddr_in clientaddr;
        socklen_t clientaddrlen = sizeof(clientaddr);

        // 接受客户端连接
        int clientfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clientaddrlen);
        if (clientfd != -1) {
    
    
            char recvBuf[32] = {
    
    0};
            // 从客户端接受数据
            int ret = recv(clientfd, recvBuf, sizeof(recvBuf), 0);
            if (ret > 0) {
    
    
                cout << "recv data from cilent successfully, data:" << recvBuf << endl;
                // 将接收到的数据原封不动地发给客户端
                ret = send(clientfd, recvBuf, strlen(recvBuf), 0);
                if (ret != strlen(recvBuf)) {
    
    
                    cout << "send data error!" << endl;
                } else {
    
    
                    cout << "send data to client successfully, data:" << recvBuf <<endl;
                }
            } else {
    
    
                cout << "recv data error!" <<endl;
            }
        }
        close(clientfd);
    }

    // 关闭监听socket
    close(listenfd);
    return 0;
}

5. 客户端代码

#include <iostream>
#include <unistd.h>		// POSIX系统API访问
#include <sys/types.h>	// 基本系统数据类型
#include <arpa/inet.h>	// 网络信息转换
#include <string.h>

using namespace std;

#define SERVER_ADDRESS "127.0.0.1"
#define SERVER_PORT 9991

int main(int argc, char* argv[]) {
    
    
    char* message;
    if(argc != 2) {
    
    
        fputs("Usage: ./client message\n", stderr); // 向指定的文件写入一个字符串
        exit(1);
    }

    message = argv[1];
    printf("send message: %s\n", message);

    // 创建一个socket
    int clientfd = socket(AF_INET, SOCK_STREAM, 0);
    if (clientfd == -1) {
    
    
        cout << "create client socket error!" << endl;
        return -1;
    }

    // 连接服务器地址
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS);
    serveraddr.sin_port = htons(SERVER_PORT);

    if (connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1) {
    
    
        cout << "connect socket error!" << endl;
        return -1;
    }

    // 向服务器发送数据
    
    int ret = send(clientfd, message, strlen(message), 0);
    if (ret != strlen(message)) {
    
    
        cout << "send data error!" << endl;
        return -1;
    } else {
    
    
        cout << "send data to server successfully, data:" << message << endl;
    }

    // 从服务器读取数据
    char recvBuf[32] = {
    
    0};
    ret = recv(clientfd, recvBuf, sizeof(recvBuf), 0);
    if (ret > 0) {
    
    
        cout << "recv data from server successfully, data:" << recvBuf << endl;
    } else {
    
    
        cout << "recv data from server error!" << endl;
    }

    // 关闭socket
    close(clientfd);
    return 0;
}

猜你喜欢

转载自blog.csdn.net/crossoverpptx/article/details/132237821