Linux 소켓 프로그래밍 (1) : 프로세스 간 TCP 통신의 간단한 실현

서버와 클라이언트는 소켓을 통해 통신합니다.

참고 블로그 :
[TCP 네트워크 프로그래밍에서 connect (), listen () 및 accept () 간의 관계]-> 다음 그림은이 블로그 (https://blog.csdn.net/tennysonsky/article/details/ 45621341)
웹 소켓 프로그래밍 기본 API
여기에 사진 설명 삽입

섬기는 사람:

  1. socket () : 소켓을 만들고 소켓 IP 주소 유형과 전송 프로토콜 유형을 설정합니다.
  2. bind () : IP 주소와 포트 번호를 소켓에 바인딩합니다.
  3. listen () : 소켓을 수동 연결 청취 소켓으로 전환합니다. 언제든지 클라이언트로부터 연결 요청을 수신 할 준비가 된 청취 소켓의 포트 번호.
    listen () 함수는 차단하지 않습니다. 주된 일은 Linux 커널에 소켓과 소켓에 해당하는 연결 대기열 길이를 알려주고 listen () 함수가 종료하는 것입니다.
  4. accept () : 클라이언트가 성공적으로 연결될 때까지 차단하고 큐에서 완료된 연결을 제거합니다.
    accept () 함수는 설정된 연결 대기열의 선두에서 완료된 연결을 가져옵니다.이 대기열에 완료된 연결이없는 경우, accept () 함수는 대기열에서 완료된 클라이언트 연결이 제거 될 때까지 현재 스레드를 차단합니다. .
  5. read () \ write (), recv () \ send () : 클라이언트와 데이터를 교환합니다.
    read ()는 블로킹 I / O 모드이며, 전체 데이터가 커널 버퍼에 수신되고 커널 버퍼에서 프로세스로 복사 될 때까지 서버 프로세스가 차단됩니다.
  6. close () : 소켓을 닫습니다. 즉, 연결을 닫습니다.

고객:

  1. socket () : 소켓을 만들고 소켓 IP 주소 유형과 전송 프로토콜 유형을 설정합니다.
    클라이언트는 연결 요청을 보내는 사람으로 포트를 고정 할 필요는 없지만 임의로 할당 할 수 있으므로 바인딩 할 필요가 없습니다.
  2. connect () : 클라이언트가 서버의 IP 주소와 포트 번호에 따라 서버와의 연결을 요청합니다.
    클라이언트는 서버에 능동적으로 접속하고 3 방향 핸드 셰이크를 통해 연결을 설정합니다.이 연결 과정은이 기능이 아닌 커널에 의해 완료됩니다.이 기능의 기능은 Linux 커널에 TCP 3 방향 핸드 셰이크 연결을 자동으로 완료하도록 알리고 마지막으로 마지막으로 연결 결과를이 함수의 반환 값으로 반환합니다 (연결 성공시 0, 실패시 -1).
  3. read () \ write () 、 recv () \ send ()
  4. 닫기()

서버 코드

주요 기능은 클라이언트가 보낸 문자열의 모든 소문자를 대문자로 변환 한 다음 클라이언트로 반환하는 것입니다. (PS : 저는 처음 배우기 때문에 코드 주석이 더 자세합니다. 사실 코드의 양은 많지 않습니다.) 참조 블로그 : Linux 소켓 프로그래밍

#include <iostream>
#include <stdio.h>
#include <cstring>       // void *memset(void *s, int ch, size_t n);
#include <sys/types.h>   // 数据类型定义
#include <sys/socket.h>  // 提供socket函数及数据结构sockaddr
#include <arpa/inet.h>   // 提供IP地址转换函数,htonl()、htons()...
#include <netinet/in.h>  // 定义数据结构sockaddr_in
#include <ctype.h>       // 小写转大写
#include <unistd.h>      // close()、read()、write()、recv()、send()...
using namespace std;

const int flag = 0; // 0表示读写处于阻塞模式
const int port = 8080;
const int buffer_size = 1<<20;


int main(int argc, const char* argv[]){
    
    
    // 创建服务器监听的套接字。Linux下socket被处理为一种特殊的文件,返回一个文件描述符。
    // int socket(int domain, int type, int protocol);
    // domain设置为AF_INET/PF_INET,即表示使用ipv4地址(32位)和端口号(16位)的组合。
    int server_sockfd = socket(PF_INET,SOCK_STREAM,0);  
    if(server_sockfd == -1){
    
    
        close(server_sockfd);
        perror("socket error!");
    }
    // /* Enable address reuse */
    // int on = 1;
    // int ret = setsockopt( server_sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on) );

    // 此数据结构用做bind、connect、recvfrom、sendto等函数的参数,指明地址信息。
    struct sockaddr_in server_addr;
    memset(&server_addr,0,sizeof(server_addr)); // 结构体清零
    server_addr.sin_family = AF_INET;  // 协议
    server_addr.sin_port = htons(port);  // 端口16位, 此处不用htons()或者错用成htonl()会连接拒绝!!
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 本地所有IP
    // 另一种写法, 假如是127.0.0.1
    // inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr.s_addr);


    // int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 
    // bind()函数的主要作用是把ip地址和端口绑定到套接字(描述符)里面
    // struct sockaddr是通用的套接字地址,而struct sockaddr_in则是internet环境下套接字的地址形式,二者长度一样,都是16个字节。二者是并列结构,指向sockaddr_in结构的指针也可以指向sockaddr。
    // 一般情况下,需要把sockaddr_in结构强制转换成sockaddr结构再传入系统调用函数中。
    if(bind(server_sockfd,(struct sockaddr*)&server_addr,sizeof(server_addr)) == -1){
    
    
        close(server_sockfd);
        perror("bind error");
    }
    // 第二个参数为相应socket可以排队的准备道来的最大连接个数
    if(listen(server_sockfd, 5) == -1){
    
    
        close(server_sockfd);
        perror("listen error");
    }

    printf("Listen on port %d\n", port);
    while(1){
    
    
        struct sockaddr_in client_addr;
        socklen_t client_len = sizeof(client_addr);
        // accept()函数从处于established状态的连接队列头部取出一个已经完成的连接,
        // 如果这个队列没有已经完成的连接,accept()函数就会阻塞当前线程,直到取出队列中已完成的客户端连接为止。
        int client_sockfd = accept(server_sockfd, (struct sockaddr*)&client_addr, &client_len);

        char ipbuf[128];
        printf("client iP: %s, port: %d\n", inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ipbuf, sizeof(ipbuf)),
            ntohs(client_addr.sin_port));

        // 实现客户端发送小写字符串给服务端,服务端将小写字符串转为大写返回给客户端
        char buf[buffer_size];
        while(1) {
    
    
            // read data, 阻塞读取
            int len = recv(client_sockfd, buf, sizeof(buf),flag);
            if (len == -1) {
    
    
                close(client_sockfd);
                close(server_sockfd);
                perror("read error");
            }else if(len == 0){
    
      // 这里以len为0表示当前处理请求的客户端断开连接
                break;
            }
            printf("read buf = %s", buf);
            // 小写转大写
            for(int i=0; i<len; ++i) {
    
    
                buf[i] = toupper(buf[i]);
            }
            printf("after buf = %s", buf);

            // 大写串发给客户端
            if(send(client_sockfd, buf, strlen(buf),flag) == -1){
    
    
                close(client_sockfd);
                close(server_sockfd);
                perror("write error");
            }
            memset(buf,'\0',len); // 清空buf
        }
        close(client_sockfd);
    }
    close(server_sockfd);

    return 0;
}

클라이언트 코드

// client 端相对简单, 另外可以使用nc命令连接->nc ip prot
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <string.h>


const int port = 8080;
const int buffer_size = 1<<20;
int main(int argc, const char *argv[]) {
    
    
    

    int client_sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (client_sockfd == -1) {
    
    
        perror("socket error");
        exit(-1);
    }
    

    struct sockaddr_in client_addr;
    bzero(&client_addr, sizeof(client_addr));
    client_addr.sin_family = AF_INET;
    client_addr.sin_port = htons(port);
    inet_pton(AF_INET, "127.0.0.1", &client_addr.sin_addr.s_addr);
    
    int ret = connect(client_sockfd, (struct sockaddr*)&client_addr, sizeof(client_addr));
    if (ret == -1) {
    
    
        perror("connect error");
        exit(-1);
    }
    
    while(1) {
    
    
        char buf[buffer_size] = {
    
    0};
        fgets(buf, sizeof(buf), stdin); // 从终端读取字符串
        write(client_sockfd, buf, strlen(buf));
        //接收, 阻塞等待
        int len = read(client_sockfd, buf, sizeof(buf));
        if (len == -1) {
    
    
             perror("read error");
             exit(-1);
        }
        printf("client recv %s\n", buf);
        
    }
    
    close(client_sockfd);
    return 0;
}

만남 문제

  1. connect error: Connection refused, 클라이언트가 서버에 연결되었을 때 연결이 거부되었습니다.
    이유 : 호스트 바이트 순서 및 네트워크 바이트 순서 변환 기능의 잘못된 사용으로 인해 포트 변환이 사용됩니다 htonl(). 포트가 16 비트 인 경우 htons () 및 ntohs ()를 사용해야하며 IP가 32 비트 인 경우 htonl () 및 ntohl ()을 사용해야합니다.
    (참고 : 데이터 스트림에는 빅 엔디안 바이트 순서와 리틀 엔디안 바이트 순서가 있습니다. TCP / IP 프로토콜은 네트워크 데이터 스트림이 빅 엔디안 바이트 순서를 채택하도록 규정하고 있습니다. 빅 엔디안 엔디안의 저장 원리 분석을 통해 char 데이터에 대해, 1 바이트 만 차지하기 때문에이 문제는 존재하지 않습니다. 이것은 데이터 버퍼가 일반적으로 문자 유형으로 정의되는 이유 중 하나입니다. IP 주소, 포트 번호와 같은 문자가 아닌 데이터의 경우 데이터를 네트워크로 전송해야합니다. 업로드 전 big-endian 모드로 전환하고 데이터 수신 후 수신 호스트에 맞는 저장 모드로 전환합니다.)
// Linux 系统为主机字节序和网络字节序的转换提供了4个函数
#include <arpa/inet.h>
/*主机字节顺序 --> 网络字节顺序*/
uint32_t htonl(uint32_t hostlong);/* IP */
uint16_t htons(uint16_t hostshort);/* 端口 */
/*网络字节顺序 --> 主机字节顺序*/
uint32_t ntohl(uint32_t netlong);/* IP */
uint16_t ntohs(uint16_t netshort);/* 端口 */
  1. bind error: Address already in use, 바인딩 된 주소 (ip + port)가 이미 사용 중입니다.
    이유 : Ctrl + Z는 작업 실행을 중단하지만 작업이 끝나지 않고 프로세스에서 일시 중단 된 상태를 유지합니다. (Ctrl + C는 프로그램 실행을 강제 종료하고 프로세스를 종료하는 것입니다.) 서버 수신 포트 8080의 상태
    netstat -anp | grep 8080보려면 사용합니다 . pid 4600을 사용하는 서버 프로세스와 4623을 사용하는 클라이언트 프로세스는 여전히 ESTABLISHED상태에 있으며 종료되지 않았습니다. kill -9 pid최종 클라이언트 및 서버 프로세스를 사용하여 포트를 해제하십시오.
    문제 : 이미 사용중인 주소

추천

출처blog.csdn.net/XindaBlack/article/details/105599656