Connecting Programming Languages to MatrixGate

This document describes how to use the API interface to connect to MatrixGate for high-speed data ingestion.


1 MatrixGate API

MatrixGate provides an HTTP API that allows various programming languages to import data into the YMatrix database via HTTP. The MatrixGate HTTP protocol format and response codes are as follows:

MatrixGate HTTP Protocol Format

Protocol Type Protocol Format Usage and Example
URL http://\:\ Specifies the mxgate connection URL
PATH / Currently supports only /; any path after / is ignored
HTTP Method POST Only POST method is supported for data loading
HTTP Header Content-Encoding: gzip Supports gzip compression of HTTP body content
Content-Type: text/plain Only text/plain is supported
HTTP Body SchemaName.TableName
Timestamp|ID|C1|C2|..|Cn
The first line in the body specifies the target table for data loading. SchemaName is optional and defaults to public. TableName is required. Starting from the second line, each line represents a row of time-series data. Columns are separated by \|, and rows are separated by \n. The first field in each row is a timestamp in Unix time (seconds). See --time-format for details. The second field is TagID (integer). Remaining fields correspond to columns in the target table. It is recommended that the target table DDL follows the column order: (Timestamp, TagID, C1, C2, ..., Cn).

MatrixGate HTTP Response Codes

Response Code Meaning Remarks
200 StatusOK Partial data format errors occurred. Error details including problematic rows and messages are returned in the response body, e.g.:
At line: 2
missing data for column "c3"
204 StatusNoContent Data successfully loaded into MatrixGate
400 StatusBadRequest Invalid request, such as incorrect POST body format, target table does not exist, or compression format mismatches HTTP header
405 StatusMethodNotAllowed Non-POST HTTP request
408 StatusTimeout Request timed out
500 StatusInternalServerErrror Database-side error; data load failed. Detailed error message included in response body
503 StatusServiceUnavailable MatrixGate rejects the request, e.g., maximum connections exceeded or MatrixGate is shutting down


2 MatrixGate HTTP API Command-Line Examples

First, create table testtable in the demo database.

=# CREATE TABLE testtable (
    time TIMESTAMP WITH TIME ZONE,
    tagid INT,
    c1 INT,
    c2 INT,
    c3 INT
    )USING MARS3
     DISTRIBUTED BY (tagid)
     ORDER BY (time,tagid);

2.1 Sending CSV Data via HTTP

Generate the mxgate.conf configuration file.

$ mxgate config --db-database testdb \
              --db-master-host localhost \
              --db-master-port 5432 \
              --db-user mxadmin \
              --db-password 123123 \
              --target public.testtable \
              --format csv \
              --time-format unix-second \
              --delimiter '|' \
              --parallel 256 \
              --stream-prepared 3 \
              --interval 250 \
              --transform plain \
              > mxgate.conf

Edit the data loading file data.txt.

$ vi data.txt
public.testtable
1603777821|1|101|201|301
1603777822|2|102|202|302
1603777823|3|103|203|303

Start mxgate with the generated configuration file mxgate.conf.

$ mxgate --config mxgate.conf

Send an HTTP request to load data.

$ curl http://localhost:8086/ -X POST -H 'Content-Type: text/plain' --data-binary "@data.txt"

Connect to the database and verify successful data load.

$ psql demo
demo=# SELECT extract(epoch FROM "time"), * FROM testtable;
 date_part  |          time          | tagid | c1  | c2  | c3
------------+------------------------+-------+-----+-----+-----
 1603777821 | 2020-10-27 13:50:21+08 |     1 | 101 | 201 | 301
 1603777822 | 2020-10-27 13:50:22+08 |     2 | 102 | 202 | 302
 1603777823 | 2020-10-27 13:50:23+08 |     3 | 103 | 203 | 303
(3 rows)

2.2 Sending JSON Data via HTTP

Add the following two key-value pairs to the HTTP POST request headers when sending data to mxgate:

"Batch-Type" = json
"Job-Name" = public.t1
  • "Batch-Type" = json indicates that the data format in the POST request body is JSON.
  • Job-Name specifies the target table name for data insertion, in the format: schemaname.tablename.

Add the following transform configuration to the mxgate configuration file:

[transform]

  ## Overall parallel level for transform, only for non-strict mode
  # parallel = 256

  ## Transform decodes input data and perform type/format conversion
  ## Types restricted to: plain/json/nil/mxmon/hanangdbc
  ## transform = "plain"
  transform = "json"
    [transform.json]
      mapping = [{
                  table-name = "public.t1",
                  field-map = [{dest="id", source="$.id", enabled=true}, {dest="content", source="$.content", enable=false}]
                }]
  • Here, source = "$.id" is a JsonPath expression. Refer to JsonPath for more information.

Assume the target table structure is as follows:

                 Table "public.t1"
 Column  |  Type   | Collation | Nullable | Default
---------+---------+-----------+----------+---------
 id      | integer |           |          |
 content | text    |           |          |
Distributed by: (id)

Sample JSON data structure:

{"id": 12345, "content": "this is a sample json"}

The mxgate JSON transform function extracts values for id and content from the JSON data based on the [transform.json] configuration, then formats them as CSV or text for loading into the database.

Result:

=# SELECT * FROM t1;
  id   |        content
-------+-----------------------
 12345 | this is a simple json
(1 row)

Currently, mxgate supports only single-line JSON strings when ingesting JSON data over HTTP. If a JSON object spans multiple lines, the load will fail.

For example, the following JSON can be transmitted:

{"employees": [{"id": 1, "name": "John Doe", "position": "Manager", "department": "Sales"},{"id": 2,"name": "Jane Smith","position": "Engineer","department": "Engineering"}]}

But the following will fail:

{
  "employees": [
    {
      "id": 1,
      "name": "John Doe",
      "position": "Manager",
      "department": "Sales"
    },
    {
      "id": 2,
      "name": "Jane Smith",
      "position": "Engineer",
      "department": "Engineering"
    }
  ]
}


3 Connecting Programming Languages to MatrixGate

3.1 JAVA Connection to MatrixGate

3.1.1 MatrixGate JAVA SDK

An SDK (Software Development Kit) frees developers from non-business logic development, significantly improving development efficiency and usability.

  1. Add SDK Dependency

You can include the SDK JAR using one of the following methods:
(1). Retrieve directly from a Maven remote repository
(2). Retrieve directly from a Gradle remote repository
(3). Manually download the JAR and import it locally

Note!
Choose only one of the above methods. Using (1) or (2) to fetch the SDK from Maven or Gradle is recommended for efficiency and convenience. For method (3), refer to MatrixGate FAQ: 21. Search by keyword JAVA SDK as this method is not commonly used and will not be detailed here.

(1). Use Maven to automatically download the SDK
Add the following dependency to your Java project's pom.xml file:

<dependencies>
    <dependency>
        <groupId>cn.ymatrix</groupId>
        <artifactId>mxgate-sdk-java</artifactId>
        <version>1.1.2</version>
    </dependency>
</dependencies>

(2). Use Gradle to include the SDK dependency

repositories {
    mavenCentral()
}

dependencies {
    implementation 'cn.ymatrix:mxgate-sdk-java:1.0.20'
}
  1. Start mxgate

YMatrix supports data ingestion via gRPC and HTTP. Details are as follows:

  • Start mxgate (gRPC Source)

Note!
A YMatrix version that supports gRPC is required, i.e., 4.6.1 or later.

On the Master node, create an mxgate configuration file mxgate_grpc.conf and start mxgate.

Note!
Before using mxgate, ensure the target table is created in the database. In this example, the Master host is mdw, the database is demo, and the table is test_table_transfer.

Example commands:

# Create mxgate config file
[mxadmin@mdw ~]$ mxgate config \
    --source grpc \
    --db-database demo \
    --target public.test_table_transfer \
    --time-format raw \
    --grpc-port 8087 \
    --format csv \
    > mxgate_grpc.conf

# Start mxgate
[mxadmin@mdw ~]$ mxgate start --config mxgate_grpc.conf

As shown, when using the SDK to write data to mxgate, set --source to grpc and --format to csv. The --grpc-port must be specified in the config so the SDK knows where to send data; in this example, port 8087 is used.

  • Start mxgate (HTTP Source)

Note!
Even when starting mxgate with HTTP, you must still specify the gRPC port because the SDK uses gRPC to retrieve table metadata from mxgate—only the data ingestion path switches to HTTP.

On the Master node, create a config file named mxgate_http.conf and start mxgate. In this example, the Master hostname is mdw:

# Create mxgate config file
[mxadmin@mdw ~]$ mxgate config \
    --source http \
    --db-database demo \
    --target public.test_table_transfer \
    --time-format raw \
    --grpc-port 8087 \
    --format csv \
    > mxgate_http.conf

# Start mxgate
[mxadmin@mdw ~]$ mxgate start --config mxgate_http.conf
  1. Send Data to mxgate Using the SDK

This section describes both asynchronous and synchronous data sending methods. Choose based on your requirements.

Example table schema:

            Partitioned table "public.test_table_transfer"
 Column |            Type             | Collation | Nullable | Default
--------+-----------------------------+-----------+----------+---------
 ts     | timestamp without time zone |           |          |
 tag    | integer                     |           | not null |
 c1     | double precision            |           |          |
 c2     | double precision            |           |          |
 c3     | double precision            |           |          |
 c4     | double precision            |           |          |
 c5     | text                        |           |          |
 c6     | text                        |           |          |
 c7     | text                        |           |          |
 c8     | text                        |           |          |
Partition key: RANGE (ts)

First, perform global initialization:

// Set log level (default: INFO)
MxLogger.loggerLevel(LoggerLevel.INFO);
// Logs are output to stdout by default. Pass false to disable.
MxLogger.enableStdout(true);
// Default log file path and name: /tmp/mxgate_sdk_java_2022-08-26_133403.log
// Users can customize the log file path and name.
MxLogger.writeToFile("/tmp/mxgate_sdk_java.log");
  • Asynchronous Data Sending with SDK

Asynchronous sending appends Tuples to an internal SDK queue, which are then sent via asynchronous HTTP requests.
Start by initializing MxBuilder. This builder follows the singleton pattern and is globally unique.

Note!
Initialize MxBuilder only once per project. Configuration can be set before building.

MxBuilder builder = MxBuilder.newBuilder()
        .withDropAll(false) // Set to true for testing (drops data instead of sending); set to false for production
        .withCacheCapacity(100000) // Queue size for micro-batch tuple storage
        .withCacheEnqueueTimeout(2000) // Timeout (ms) for enqueueing tuples if queue is full; throws IllegalStateException on timeout
        .withConcurrency(10) // Number of threads concurrently writing to mxgate
        .withRequestTimeoutMillis(3000) // Request timeout per thread (milliseconds)
        .withMaxRetryAttempts(3) // Retry attempts per failed write request
        .withRetryWaitDurationMillis(3000) // Fixed interval between retries
        .withRequestType(RequestType.WithHTTP) // Supported types: RequestType.WithHTTP, RequestType.WithGRPC
        .withCircuitBreaker() // Enable built-in circuit breaker
        .withMinimumNumberOfCalls(1) // Minimum calls before circuit breaker activates (>=1, default 10)
        .withSlidingWindowSize(10) // Sliding window size for failure rate calculation (>=1, default 100)
        .withFailureRateThreshold(60.0f) // Failure rate threshold (%) to trigger circuit breaker
        .withSlowCallDurationThresholdMillis(1000) // Duration threshold for slow requests (ms), should be less than request timeout
        .withSlowCallRateThreshold(80.0f) // Slow call rate threshold (%) to trigger circuit breaker
        // .withRequestAsync(true)// Enable async worker threads (uncomment to use)
        // .withConcurrency(20)// Async mode typically achieves high throughput with moderate concurrency (uncomment to use)
        // New API in MxBuilder for CSV construction parallelism (MxClient Group level)
        // .withCSVConstructionParallel(100)// Available from v1.1.0 (uncomment to use)
        .build();
        // New singleton API in MxBuilder: after successful build, retrieve the global instance anywhere
        // MxBuilder.instance();// Available from v1.1.2 (uncomment to use)

    // builder.getTupleCacheSize(); // Get current number of remaining tuples in SDK cache (uncomment to use)

The connect method of MxBuilder takes four parameters:
(1). Hostname (or IP) and port where mxgate provides services for data ingestion and metadata retrieval.
For HTTP: http://localhost:8086/; for gRPC: localhost:8087.
(2). Schema name.
(3). Table name.
(4). Callback: On success, onSuccess returns an MxClient instance for data writes; on failure, onFailure returns a failure message.

Example code (replace port, schema, and table names accordingly):

// Asynchronous method (available from v1.0.13)
builder.connect("http://localhost:8086/", "localhost:8087", "public", "test_table", new ConnectionListener() {
    @Override
    public void onSuccess(MxClient client) {
        sendData(client);
    }

    @Override
    public void onFailure(String failureMsg) {

    }
});

Through the onSuccess() callback of ConnectionListener, obtain the MxClient instance and use its APIs to write data to mxgate.

// Synchronous method (available from v1.0.13)
MxClient client = builder.connect(httpHost, gRPCHost, schema, table);
/* 
 * v1.1.0 enhances MxClient extensibility with new MxBuilder APIs.
 * These allow customizing MxClient Group count, tuple cache capacity, and consumer concurrency.
 * (Available from v1.1.0)
 */
// MxClient client = builder.connectWithGroup(dataSendingHost, metadataHost, schema, table, 10);
// MxClient client = builder.connectWithGroup(dataSendingHost, metadataHost, schema, table, 1, 1000, 3000, 10);// (Available from v1.1.0, uncomment to use)

/* 
 * skip-prefixed APIs skip backend connection for metadata retrieval.
 * Such MxClient instances can only generate lightweight Tuples via generateEmptyTupleLite().
 * (Available from v1.1.0) 
 */
// MxClient client = mxBuilder.skipConnectWithGroup(dataSendingHost, metadataHost, schema, table, delimiter, 1);
// MxClient client = mxBuilder.skipConnectWithGroup(dataSendingHost, metadataHost, schema, table, delimiter, 1, 1000, 3000, 1);

Note!
MxClient is thread-safe. However, for optimal performance in multi-threaded environments, it is best to have one MxClient instance per thread—obtained by calling connect multiple times on the same MxBuilder.

private void sendData(MxClient client) {
    if (client != null) {
        /*
         * MxClient batches tuples into micro-batches for sending.
         * This sets the flush interval in milliseconds (default: 2000).
         * A batch is sent every 2 seconds, but only if data has been written.
         */
        client.withIntervalToFlushMillis(5000);

        /*
         * Sets the byte size threshold for flushing a micro-batch.
         * A flush occurs when either the byte limit is reached or the time interval expires.
         */
        client.withEnoughBytesToFlush(500);

        /*
         * Improves append performance by avoiding byte counting.
         * Use based on scenario. If withEnoughBytesToFlush meets performance needs,
         * data per flush will be more uniform.
         * withEnoughLinesToFlush takes precedence over withEnoughBytesToFlush.
         */
        client.withEnoughLinesToFlush(10000);

        /*
         * New API in MxClient to control CSV conversion subtask size.
         * E.g., 10,000 tuples in a flush, with BatchSize=2000 → 5 concurrent subtasks.
         * (Available from v1.1.0)
         */
        client.withCSVConstructionBatchSize(2000);

        /*
         * Each MxClient has a private object pool.
         * Size should be tuned based on typical flush volume.
         */
        // client.useTuplesPool(poolSize);

        /*
         * Compression support requires mxgate v4.7.6 or higher.
         */
        // client.withCompress();

        /* 
         * Base64 encoding is optional for HTTP, required for gRPC.
         */
        // client.withBase64Encode4Compress(); 

        /*
         * Register a DataPostListener to receive callbacks on batch success/failure.
         * You can track which tuples succeeded or failed.
         */
        client.registerDataPostListener(new DataPostListener() {
            @Override
            public void onSuccess(Result result) {
                System.out.println(CUSTOMER_LOG_TAG + "Send tuples success: " + result.getMsg());
                System.out.println(CUSTOMER_LOG_TAG + "Succeed lines onSuccess callback " + result.getSucceedLines());
            }

            @Override
            public void onFailure(Result result) {
                /* 
                 * result.getErrorTuplesMap() contains map of failed tuples and error messages.
                 * Key: failed tuple, Value: error reason.
                 */
                for (Map.Entry<Tuple, String> entry : result.getErrorTuplesMap().entrySet()) {
                    l.error(CUSTOMER_LOG_TAG + "error tuple of table={}, tuple={}, reason={}", entry.getKey().getTableName(), entry.getKey(), entry.getValue());
                    for (Column c : entry.getKey().getColumns()) {
                        l.error(CUSTOMER_LOG_TAG + "error entry columns {} = {}", c.getColumnName(), c.getValue());
            }
        }
        System.out.println(result.getSuccessLines());
    }
}

Use generateEmptyTuple API of MxClient to get an empty Tuple for value assignment.

Tuple tuple1 = client.generateEmptyTuple();
Tuple tuple2 = client.generateEmptyTuple();

Assign values using Key -> Value pairs. Key is the column name in the database table; Value is the field value. Fields with NULL or defaults may be omitted—the SDK fills them automatically.

Tuples obtained via this API do not maintain table metadata internally, making them lightweight. When calling MxClient.appendTuple(), skipping metadata validation improves flush speed.

// Tuple tuple = client.generateEmptyTupleLite();// (Available from v1.1.0, uncomment to use)

Note!
When adding columns to such lightweight Tuples, manually maintain the order of addColumn() key -> value to match the exact column order in the database table.

For example, if the table column order is:

  Column |            Type             | Collation | Nullable | Default
 --------+-----------------------------+-----------+----------+---------
  ts     | timestamp without time zone |           |          |
  tag    | integer                     |           | not null |
  c1     | double precision            |           |          |
  c2     | double precision            |           |          |
  c3     | double precision            |           |          |
  c4     | double precision            |           |          |
  c5     | text                        |           |          |
  c6     | text                        |           |          |
  c7     | text                        |           |          |
  c8     | text                        |           |          |

Then column additions must follow this order, as in:

        tuple1.addColumn("ts", "2022-05-18 16:30:06");
        tuple1.addColumn("tag", 102020030);
        tuple1.addColumn("c1", 1.1);
        tuple1.addColumn("c2", 2.2);
        tuple1.addColumn("c3", 3.3);
        tuple1.addColumn("c4", 4.4);
        tuple1.addColumn("c5", "中文字符测试-1");
        tuple1.addColumn("c6", "lTxFCVLwcDTKbNbjau_c6");
        tuple1.addColumn("c7", "lTxFCVLwcDTKbNbjau_c7");
        tuple1.addColumn("c8", "lTxFCVLwcDTKbNbjau_c8");

        tuple2.addColumn("ts", "2022-05-18 16:30:06");
        tuple2.addColumn("tag", 102020030);
        tuple2.addColumn("c1", 1.1);
        tuple2.addColumn("c2", 2.2);
        tuple2.addColumn("c3", 3.3);
        tuple2.addColumn("c4", 4.4);
        tuple2.addColumn("c5", "中文字符测试-2");
        tuple2.addColumn("c6", "lTxFCVLwcDTKbNbjau_c26");
        tuple2.addColumn("c7", "lTxFCVLwcDTKbNbjau_c27");
        tuple2.addColumn("c8", "lTxFCVLwcDTKbNbjau_c28");

Finally, append the populated Tuples to MxClient.

client.appendTuples(tuple1, tuple2);

Note!
MxClient provides multiple APIs: append one Tuple, multiple Tuples, or a List of Tuples (e.g., client.appendTuple();, client.appendTupleList();). You can also manually trigger a flush using flush(), regardless of how many Tuples are queued, e.g., client.flush();.

  • Synchronous Data Sending with MxClient

Synchronous sending blocks until the Tuple is sent to mxgate, enabling downstream actions (e.g., committing Kafka offsets).

Tuple tuple1 = client.generateEmptyTuple();
tuple1.addColumn("ts", "2022-05-18 16:30:06");
tuple1.addColumn("tag", 102020030);
tuple1.addColumn("c1", 1.1);
tuple1.addColumn("c2", 2.2);
tuple1.addColumn("c3", 3.3);
tuple1.addColumn("c4", 4.4);
tuple1.addColumn("c5", "lTxFCVLwcDTKbNbjau_c5");
tuple1.addColumn("c6", "lTxFCVLwcDTKbNbjau_c6");
tuple1.addColumn("c7", "lTxFCVLwcDTKbNbjau_c7");
tuple1.addColumn("c8", "lTxFCVLwcDTKbNbjau_c8");

/* 
 * appendTupleBlocking returns boolean:
 * true: MxClient buffer is full (ready to send)
 * false: Buffer not yet full
 */
try {
    if (client.appendTupleBlocking(tuple1)) {
        l.info("append tuples enough");
        // Manually trigger flush       
        client.flushBlocking();
    }
/* 
 * If flushBlocking throws exception, entire batch failed. Handle accordingly.
 */
} catch (AllTuplesFailException e) {
    l.error("Tuples fail and catch the exception return.", e);
    for (Map.Entry<Tuple, String> entry : result.getErrorTuplesMap().entrySet()) {
        l.error(CUSTOMER_LOG_TAG + "error tuple of table={}, tuple={}, reason={}", entry.getKey().getTableName(), entry.getKey(), entry.getValue());
        for (Column c : entry.getKey().getColumns()) {
            l.error(CUSTOMER_LOG_TAG + "error entry columns {} = {}", c.getColumnName(), c.getValue());
  }
}                  
/*
 * If flushBlocking throws exception, some tuples failed. Handle accordingly.
 */
} catch (PartiallyTuplesFailException e) {
    for (Map.Entry<Tuple, String> entry : result.getErrorTuplesMap().entrySet()) {
    l.error(CUSTOMER_LOG_TAG + "error tuple of table={}, tuple={}, reason={}", entry.getKey().getTableName(), entry.getKey(), entry.getValue());
        for (Column c : entry.getKey().getColumns()) {
            l.error(CUSTOMER_LOG_TAG + "error entry columns {} = {}", c.getColumnName(), c.getValue());
  }
 }
}

3.1.2 MatrixGate HTTP API Java Example

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

public class MxgateExample {
    public static void main(String[] args) throws Exception {
        MxgateExample http = new MxgateExample();
        http.sendingPostRequest();
    }

    /* 
     * HTTP Post request
     */
    private void sendingPostRequest() throws Exception {

        /* 
         * mxgate listens on port 8086 of localhost
         */
        String url = "http://localhost:8086/";
        URL obj = new URL(url);
        HttpURLConnection con = (HttpURLConnection) obj.openConnection();

        /* 
         * Setting basic post request
         */
        con.setRequestMethod("POST");
        con.setRequestProperty("Content-Type","text/plain");
        String postJsonData = "public.testtable\n1603777821|1|101|201|301\n1603777822|2|102|202|302\n1603777823|3|103|203|303";

        con.setDoOutput(true);
        DataOutputStream wr = new DataOutputStream(con.getOutputStream());

        /*
         * When data contains Chinese characters, encode using postJsonData.getBytes("UTF-8")
         */
        wr.write(postJsonData.toString().getBytes("UTF-8"));
        wr.flush();
        wr.close();

        int responseCode = con.getResponseCode();
        System.out.println("Sending 'POST' request to URL : " + url);
        System.out.println("Post Data : " + postJsonData);
        System.out.println("Response Code : " + responseCode);

        BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
        String output;
        StringBuffer response = new StringBuffer();

        while ((output = in.readLine()) != null) {
            response.append(output);
        }
        in.close();

        System.out.println(response.toString());
    }
}


3.2 MatrixGate HTTP API Python Example

import http.client

class MxgateExample(object):
    def __init__(self):

        /*
         * mxgate listens on port 8086 of localhost
         */
        self.url = "localhost:8086"

        self.postData = "public.testtable\n/" \
                        "1603777821|1|101|201|301\n/" \
                        "1603777822|2|102|202|302\n/" \
                        "1603777823|3|103|203|303"
        self.headers = {"Content-Type": "text/plain"}

    /* 
     * HTTP Post request
     */
    def sending_post_request(self):

        conn = http.client.HTTPConnection(self.url)
        conn.request("POST", "/", self.postData, self.headers)

        response = conn.getresponse()
        response_code = response.getcode()
        print(f"Sending 'POST' request to URL : {self.url}")
        print(f"Post Data : {self.postData}")
        print(f"Response Code : {response_code}")

        output = response.read()
        print(output)

if __name__ == '__main__':
    gate_post = MxgateExample()
    gate_post.sending_post_request()


3.3 MatrixGate HTTP API C# Example

It is recommended to use the C# Core development environment.

using System;
using System.IO;
using System.Net;
using System.Text;

namespace HttpPostTest
{
class Program
    {
        static void Main(string[] args)
        {
            var url = "http://10.13.2.177:8086/";
            var txt = "public.dest\n2021-01-01 00:00:00,1,a1\n2021-01-01 00:00:00,2,a2\n2021-01-01 00:00:00,3,a3";

           HttpPost(url,txt);
        }

public static string HttpPost(string url, string content){
    string result = "";
    HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url);
    req.Method = "POST";
    req.ContentType = "text/plain";

    /*
     * region Add Post parameters
     */
    byte[] data = Encoding.UTF8.GetBytes(content);
    req.ContentLength = data.Length;
    using (Stream reqStream = req.GetRequestStream()){
        reqStream.Write(data, 0, data.Length);
        reqStream.Close();
    }

    HttpWebResponse resp = (HttpWebResponse)req.GetResponse();
    Stream stream = resp.GetResponseStream();

    /* 
     * Get response content
     */
    using (StreamReader reader = new StreamReader(stream, Encoding.UTF8)){
        result = reader.ReadToEnd();
        }
        return result;
    }
  }
}

If you encounter the error error when serving connection ***** body size exceeds the given limit, increase the value of max-body-bytes under mxgate.conf.

3.4 MatrixGate HTTP API Golang Example

package main

import (
    "bytes"
    "net/http"
)

func PostDataToServer(URL string) error {
    data := `public.testtable
            1603777821|1|101|201|301
            1603777822|2|102|202|302
            1603777823|3|103|203|303
            `
    resp, err := http.Post(URL, "application/text", bytes.NewBuffer([]byte(data)))
    if err != nil {
        return err
    }
    if resp.StatusCode != 200 {
        /* 
         * Deal with the response body.
         */
        return nil
    }

    /*
     * Deal with the response body.
     */
    return nil
}

func main()  {
    err := PostDataToServer("http://127.0.0.1:8086")
    if err != nil{
        panic(err)
    }

}

Note!
For more MatrixGate features, see Tools Guide - mxgate.