complete rewrite

This commit is contained in:
Nishi 2026-01-07 04:24:22 +09:00
commit 8a717a2b2e
Signed by: nishi
GPG key ID: 27EF69B208EB9343
41 changed files with 2625 additions and 1255 deletions

20
engine/net/.clang-format Normal file
View file

@ -0,0 +1,20 @@
---
Language: Cpp
UseTab: Always
TabWidth: 8
AlignConsecutiveAssignments:
Enabled: true
AlignConsecutiveDeclarations:
Enabled: true
IndentWidth: 8
PointerAlignment: Left
ColumnLimit: 0
AllowShortIfStatementsOnASingleLine: Always
AllowShortBlocksOnASingleLine: Never
AllowShortFunctionsOnASingleLine: Empty
AllowShortLoopsOnASingleLine: true
SpaceBeforeParens: Never
AlignEscapedNewlines: DontAlign
SortIncludes: false
AllowShortEnumsOnASingleLine: false
#IndentPPDirectives: AfterHash

21
engine/net/CMakeLists.txt Normal file
View file

@ -0,0 +1,21 @@
cmake_minimum_required(VERSION 3.11)
project(libfishsoup)
add_library(fishsoup SHARED src/client.c src/server.c src/socket.c src/stb_ds.c)
target_include_directories(fishsoup PUBLIC include)
find_package(PkgConfig)
pkg_check_modules(ZLIB REQUIRED zlib)
target_compile_options(fishsoup PRIVATE ${ZLIB_CFLAGS_OTHER})
target_include_directories(fishsoup PRIVATE ${ZLIB_INCLUDE_DIRS})
target_link_options(fishsoup PRIVATE ${ZLIB_LDFLAGS_OTHER})
target_link_directories(fishsoup PRIVATE ${ZLIB_LIBRARY_DIRS})
target_link_libraries(fishsoup PRIVATE ${ZLIB_LIBRARIES})
include(GNUInstallDirs)
install(
TARGETS fishsoup
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
)

24
engine/net/LICENSE Normal file
View file

@ -0,0 +1,24 @@
Copyright (c) 2025, Fishsoup
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the <organization> nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

33
engine/net/Makefile Normal file
View file

@ -0,0 +1,33 @@
CC = cc
CFLAGS = -g -Iinclude -fPIC `pkg-config --cflags zlib`
LDFLAGS = -shared
LIBS = `pkg-config --libs zlib`
OBJS = src/client.o src/server.o src/socket.o src/stb_ds.o
LIB = lib
SO = .so
.PHONY: all format clean
.SUFFIXES: .c .o
all: $(LIB)fishsoup$(SO)
format:
clang-format --verbose -i `find src include "(" -name "*.c" -or -name "*.h" ")" -and -not -name "stb_*.h"` test*.c
$(LIB)fishsoup$(SO): $(OBJS)
$(CC) $(LDFLAGS) -o $@ $(OBJS) $(LIBS)
testclient: testclient.c $(LIB)fishsoup$(SO)
cc -o $@ testclient.c -Wl,-R./ -L./ -Iinclude -lfishsoup
testserver: testserver.c $(LIB)fishsoup$(SO)
cc -o $@ testserver.c -Wl,-R./ -L./ -Iinclude -lfishsoup
.c.o:
$(CC) $(CFLAGS) -c -o $@ $<
clean:
rm -f *$(SO) $(OBJS) ./testserver ./testclient

View file

@ -0,0 +1,86 @@
#ifndef __FISHSOUP_H__
#define __FISHSOUP_H__
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <time.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <netinet/tcp.h>
#include <poll.h>
/* TCP */
#define FISHSOUP_PORT 3219
#define FISHSOUP_PING 3
enum fishsoup_packet_type {
FISHSOUP_PACKET_PING = 0,
FISHSOUP_PACKET_PONG,
FISHSOUP_PACKET_USER
};
typedef struct fishsoup_client fishsoup_client_t;
typedef struct fishsoup_server fishsoup_server_t;
typedef struct fishsoup_packet_header fishsoup_packet_header_t;
typedef struct fishsoup_packet fishsoup_packet_t;
struct fishsoup_client {
int fd;
time_t last_ping;
void (*on_packet)(fishsoup_client_t* client, fishsoup_packet_t* packet);
void (*on_disconnect)(fishsoup_client_t* client);
void* user;
struct {
int sent_ping;
int cleanup;
} server;
};
struct fishsoup_server {
int fd;
struct sockaddr_in address;
fishsoup_client_t* clients;
void* user;
void (*on_client)(fishsoup_server_t* server, fishsoup_client_t* client);
void (*on_tick)(fishsoup_server_t* server);
};
#pragma pack(1)
struct fishsoup_packet_header {
unsigned char type;
unsigned short length; /* ok, seriously, who sends 64k at ONCE */
};
#pragma pack()
/* !!! REMEMBER TO FREE DATA AFTER RECV UNLESS IT'S NULL !!! */
struct fishsoup_packet {
fishsoup_packet_header_t header;
void* data;
};
fishsoup_client_t* fishsoup_client(const char* host, int port);
void fishsoup_client_on_packet(fishsoup_client_t* client, void (*on_packet)(fishsoup_client_t* client, fishsoup_packet_t* packet));
void fishsoup_client_on_disconnect(fishsoup_client_t* client, void (*on_disconnect)(fishsoup_client_t* client));
int fishsoup_client_poll(fishsoup_client_t* client);
void fishsoup_client_destroy(fishsoup_client_t* client);
fishsoup_server_t* fishsoup_server(int port);
int fishsoup_server_clients(fishsoup_server_t* server);
void fishsoup_server_loop(fishsoup_server_t* server);
void fishsoup_server_on_client(fishsoup_server_t* server, void (*on_client)(fishsoup_server_t* server, fishsoup_client_t* client));
void fishsoup_server_on_tick(fishsoup_server_t* server, void (*on_tick)(fishsoup_server_t* server));
void fishsoup_server_broadcast(fishsoup_server_t* server, fishsoup_packet_t* packet);
void fishsoup_server_destroy(fishsoup_server_t* server);
int fishsoup_fd_send(int fd, const void* buf, int length);
int fishsoup_fd_recv(int fd, void* buf, int length, int to);
int fishsoup_packet_send(int fd, fishsoup_packet_t* packet);
int fishsoup_packet_recv(int fd, fishsoup_packet_t* packet);
#endif

93
engine/net/src/client.c Normal file
View file

@ -0,0 +1,93 @@
#include <fishsoup.h>
fishsoup_client_t* fishsoup_client(const char* host, int port) {
fishsoup_client_t* client = malloc(sizeof(*client));
struct addrinfo hints;
char p[32];
struct addrinfo* result;
struct addrinfo* rp;
if(port == 0) port = FISHSOUP_PORT;
sprintf(p, "%d", port);
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
if(getaddrinfo(host, p, &hints, &result) != 0) {
free(client);
return NULL;
}
for(rp = result; rp != NULL; rp = rp->ai_next) {
int val = 1;
if((client->fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol)) < 0) {
continue;
}
val = 1;
setsockopt(client->fd, IPPROTO_TCP, TCP_NODELAY, (char*)&val, sizeof(val));
if(connect(client->fd, rp->ai_addr, rp->ai_addrlen) >= 0) {
break;
}
close(client->fd);
}
if(rp == NULL) {
free(client);
return NULL;
}
client->on_packet = NULL;
client->on_disconnect = NULL;
return client;
}
void fishsoup_client_on_packet(fishsoup_client_t* client, void (*on_packet)(fishsoup_client_t* client, fishsoup_packet_t* packet)) {
client->on_packet = on_packet;
}
void fishsoup_client_on_disconnect(fishsoup_client_t* client, void (*on_disconnect)(fishsoup_client_t* client)) {
client->on_disconnect = on_disconnect;
}
int fishsoup_client_poll(fishsoup_client_t* client) {
struct pollfd pfd;
fishsoup_packet_t pkt;
int ret;
pfd.fd = client->fd;
pfd.events = POLLIN | POLLPRI;
ret = poll(&pfd, 1, 0);
if(ret <= 0) return ret;
ret = fishsoup_packet_recv(client->fd, &pkt);
if(ret == 0) return -1; /* connection broke, probably */
if(pkt.header.type == FISHSOUP_PACKET_PING) {
fishsoup_packet_t ret;
client->last_ping = time(NULL);
ret.header.type = FISHSOUP_PACKET_PONG;
ret.header.length = 0;
ret.data = NULL;
if(fishsoup_packet_send(client->fd, &ret) == 0) {
if(pkt.data != NULL) free(pkt.data);
return -1;
}
}
if(client->on_packet != NULL) client->on_packet(client, &pkt);
if(pkt.data != NULL) free(pkt.data);
return 1;
}
void fishsoup_client_destroy(fishsoup_client_t* client) {
if(client->on_disconnect != NULL) client->on_disconnect(client);
close(client->fd);
free(client);
}

199
engine/net/src/server.c Normal file
View file

@ -0,0 +1,199 @@
#include <fishsoup.h>
#include "stb_ds.h"
fishsoup_server_t* fishsoup_server(int port) {
fishsoup_server_t* server = malloc(sizeof(*server));
int val;
if(port == 0) port = FISHSOUP_PORT;
if((server->fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
free(server);
return NULL;
}
val = 1;
setsockopt(server->fd, IPPROTO_TCP, TCP_NODELAY, (char*)&val, sizeof(val));
server->address.sin_family = AF_INET;
server->address.sin_addr.s_addr = INADDR_ANY;
server->address.sin_port = htons(port);
if(bind(server->fd, (struct sockaddr*)&server->address, sizeof(server->address)) < 0) {
close(server->fd);
free(server);
return NULL;
}
if(listen(server->fd, 128) < 0) {
close(server->fd);
free(server);
return NULL;
}
server->clients = NULL;
server->on_client = NULL;
server->on_tick = NULL;
return server;
}
int fishsoup_server_clients(fishsoup_server_t* server) {
return arrlen(server->clients);
}
void fishsoup_server_loop(fishsoup_server_t* server) {
struct pollfd* pfd;
while(1) {
int i;
int ret;
pfd = malloc(sizeof(*pfd) * (1 + arrlen(server->clients)));
pfd[0].fd = server->fd;
pfd[0].events = POLLIN | POLLPRI;
for(i = 0; i < arrlen(server->clients); i++) {
pfd[1 + i].fd = server->clients[i].fd;
pfd[1 + i].events = POLLIN | POLLPRI;
}
ret = poll(pfd, 1 + arrlen(server->clients), 50);
if(ret < 0) {
free(pfd);
break;
}
if(server->on_tick != NULL) server->on_tick(server);
if(pfd[0].revents & POLLIN) {
fishsoup_client_t client;
struct sockaddr_in address;
int len = sizeof(address);
client.fd = accept(server->fd, (struct sockaddr*)&address, &len);
client.last_ping = time(NULL);
client.server.cleanup = 0;
client.server.sent_ping = 0;
client.on_packet = NULL;
arrput(server->clients, client);
if(server->on_client != NULL) server->on_client(server, &server->clients[arrlen(server->clients) - 1]);
#ifdef DEBUG
fprintf(stderr, "Client %d accepted\n", server->clients[i].fd);
#endif
}
for(i = 0; i < arrlen(server->clients); i++) {
time_t diff;
if(server->clients[i].server.cleanup) continue;
if(pfd[1 + i].revents & POLLIN) {
fishsoup_packet_t pkt;
if(fishsoup_packet_recv(server->clients[i].fd, &pkt) == 0) {
server->clients[i].server.cleanup = 1;
} else {
if(pkt.header.type == FISHSOUP_PACKET_PONG && server->clients[i].server.sent_ping) {
server->clients[i].last_ping = time(NULL);
server->clients[i].server.sent_ping = 0;
#ifdef DEBUG
fprintf(stderr, "Pong from client %d\n", server->clients[i].fd);
#endif
} else if(pkt.header.type == FISHSOUP_PACKET_PING) {
fishsoup_packet_t ret;
ret.header.type = FISHSOUP_PACKET_PONG;
ret.header.length = 0;
ret.data = NULL;
if(fishsoup_packet_send(server->clients[i].fd, &ret) == 0) {
server->clients[i].server.cleanup = 1;
} else {
#ifdef DEBUG
fprintf(stderr, "Ping from client %d, sending back pong\n", server->clients[i].fd);
#endif
}
}
if(!server->clients[i].server.cleanup && server->clients[i].on_packet != NULL) server->clients[i].on_packet(&server->clients[i], &pkt);
if(pkt.data != NULL) free(pkt.data);
}
}
if(server->clients[i].server.cleanup) continue;
diff = time(NULL) - server->clients[i].last_ping;
if(diff == FISHSOUP_PING && !server->clients[i].server.sent_ping) {
fishsoup_packet_t pkt;
pkt.header.type = FISHSOUP_PACKET_PING;
pkt.header.length = 0;
pkt.data = NULL;
if(fishsoup_packet_send(server->clients[i].fd, &pkt) == 0) {
server->clients[i].server.cleanup = 1;
#ifdef DEBUG
fprintf(stderr, "Failed to ping client %d\n", server->clients[i].fd);
#endif
} else {
server->clients[i].server.sent_ping = 1;
#ifdef DEBUG
fprintf(stderr, "Pinging client %d\n", server->clients[i].fd);
#endif
}
} else if(diff == FISHSOUP_PING * 2 && server->clients[i].server.sent_ping) {
server->clients[i].server.cleanup = 1;
#ifdef DEBUG
fprintf(stderr, "Ping timeout (client %d)\n", server->clients[i].fd);
#endif
}
}
for(i = 0; i < arrlen(server->clients); i++) {
if(server->clients[i].server.cleanup) {
#ifdef DEBUG
fprintf(stderr, "Client %d cleanup\n", server->clients[i].fd);
#endif
if(server->clients[i].on_disconnect != NULL) server->clients[i].on_disconnect(&server->clients[i]);
close(server->clients[i].fd);
arrdel(server->clients, i);
i--;
}
}
free(pfd);
}
}
void fishsoup_server_on_client(fishsoup_server_t* server, void (*on_client)(fishsoup_server_t* server, fishsoup_client_t* client)) {
server->on_client = on_client;
}
void fishsoup_server_on_tick(fishsoup_server_t* server, void (*on_tick)(fishsoup_server_t* server)) {
server->on_tick = on_tick;
}
void fishsoup_server_destroy(fishsoup_server_t* server) {
int i;
for(i = 0; i < arrlen(server->clients); i++) {
if(server->clients[i].on_disconnect != NULL) server->clients[i].on_disconnect(&server->clients[i]);
close(server->clients[i].fd);
}
arrfree(server->clients);
close(server->fd);
free(server);
}
void fishsoup_server_broadcast(fishsoup_server_t* server, fishsoup_packet_t* packet) {
int i;
for(i = 0; i < arrlen(server->clients); i++) {
if(fishsoup_packet_send(server->clients[i].fd, packet) == 0) {
server->clients[i].server.cleanup = 1;
}
}
}

200
engine/net/src/socket.c Normal file
View file

@ -0,0 +1,200 @@
#include <fishsoup.h>
#include <zlib.h>
#define CHUNK 512
int fishsoup_packet_send(int fd, fishsoup_packet_t* packet) {
int r;
fishsoup_packet_header_t h;
int len = 0;
unsigned char* data = NULL;
if(packet->header.length > 0) {
z_stream strm;
int in = packet->header.length;
int seek = 0;
int flush = 0;
unsigned char buf[CHUNK];
strm.zalloc = Z_NULL;
strm.zfree = Z_NULL;
strm.opaque = Z_NULL;
if(deflateInit(&strm, Z_DEFAULT_COMPRESSION) != Z_OK) return 0;
do {
strm.avail_in = in <= CHUNK ? in : CHUNK;
strm.next_in = ((unsigned char*)packet->data) + seek;
flush = in <= CHUNK ? Z_FINISH : Z_NO_FLUSH;
do {
int have;
strm.avail_out = CHUNK;
strm.next_out = buf;
deflate(&strm, flush);
have = CHUNK - strm.avail_out;
if(data == NULL) {
data = malloc(have);
memcpy(data, buf, have);
} else {
unsigned char* old = data;
data = malloc(len + have);
memcpy(data, old, len);
memcpy(data + len, buf, have);
free(data);
}
len += have;
} while(strm.avail_out == 0);
seek += CHUNK;
} while(flush != Z_FINISH);
deflateEnd(&strm);
}
memcpy(&h, &packet->header, sizeof(h));
h.length = htons(len);
r = fishsoup_fd_send(fd, &h, sizeof(h));
if(r != sizeof(h)) return 0;
if(data != NULL) {
r = fishsoup_fd_send(fd, data, len);
if(r != sizeof(h)) {
free(data);
return 0;
}
free(data);
}
return sizeof(h) + len;
}
int fishsoup_packet_recv(int fd, fishsoup_packet_t* packet) {
fishsoup_packet_header_t h;
if(fishsoup_fd_recv(fd, &h, sizeof(h), 500) != sizeof(h)) {
packet->data = NULL;
return 0;
}
memcpy(&packet->header, &h, sizeof(h));
packet->header.length = ntohs(packet->header.length);
packet->data = NULL;
if(packet->header.length > 0) {
unsigned char* data = malloc(packet->header.length);
z_stream strm;
int in = packet->header.length;
int ret;
int seek = 0;
int len = 0;
unsigned char buf[CHUNK];
if(fishsoup_fd_recv(fd, data, packet->header.length, 50) != packet->header.length) {
free(data);
return 0;
}
strm.zalloc = Z_NULL;
strm.zfree = Z_NULL;
strm.opaque = Z_NULL;
strm.avail_in = 0;
strm.next_in = Z_NULL;
if(inflateInit(&strm) != Z_OK) {
free(data);
return 0;
}
do {
strm.avail_in = in <= CHUNK ? in : CHUNK;
strm.next_in = data + seek;
do {
int have;
strm.avail_out = CHUNK;
strm.next_out = buf;
ret = inflate(&strm, Z_NO_FLUSH);
if(ret == Z_NEED_DICT || ret == Z_DATA_ERROR || ret == Z_MEM_ERROR) {
inflateEnd(&strm);
if(packet->data != NULL) {
free(packet->data);
packet->data = NULL;
}
free(data);
return 0;
}
have = CHUNK - strm.avail_out;
if(packet->data == NULL) {
packet->data = malloc(have);
memcpy(packet->data, buf, have);
} else {
unsigned char* old = packet->data;
packet->data = malloc(len + have);
memcpy(packet->data, old, len);
memcpy(packet->data + len, buf, have);
free(packet->data);
}
len += have;
} while(strm.avail_out == 0);
} while(ret != Z_STREAM_END);
inflateEnd(&strm);
free(data);
packet->header.length = len;
}
return sizeof(h) + packet->header.length;
}
int fishsoup_fd_send(int fd, const void* buf, int length) {
struct pollfd pfd;
pfd.fd = fd;
pfd.events = POLLOUT | POLLPRI;
poll(&pfd, 1, 0);
if(!(pfd.revents & POLLOUT)) return 0;
return send(fd, buf, length, 0);
}
int fishsoup_fd_recv(int fd, void* buf, int length, int to) {
struct pollfd pfd;
int ret;
unsigned char* b = buf;
int i = 0;
time_t t = time(NULL);
pfd.fd = fd;
pfd.events = POLLIN | POLLPRI;
while(i < length) {
/* i think 500 is reasonable timeout. no? */
ret = poll(&pfd, 1, to);
if((time(NULL) - t) >= 5) return i; /* we waited enough! kill the connection */
if(ret <= 0) return i;
ret = recv(fd, &b[i], 1, 0);
if(ret < 1) return i;
i++;
}
return i;
}

2
engine/net/src/stb_ds.c Normal file
View file

@ -0,0 +1,2 @@
#define STB_DS_IMPLEMENTATION
#include "stb_ds.h"

1895
engine/net/src/stb_ds.h Normal file

File diff suppressed because it is too large Load diff

11
engine/net/testclient.c Normal file
View file

@ -0,0 +1,11 @@
#include <fishsoup.h>
void on(fishsoup_client_t* client, fishsoup_packet_t* pkt) {
if(pkt->header.type == FISHSOUP_PACKET_USER) printf("Received: %s\n", pkt->data);
}
int main() {
fishsoup_client_t* client = fishsoup_client("127.0.0.1", 0);
fishsoup_client_on_packet(client, on);
while(fishsoup_client_poll(client) >= 0);
}

18
engine/net/testserver.c Normal file
View file

@ -0,0 +1,18 @@
#include <fishsoup.h>
void on(fishsoup_server_t* server, fishsoup_client_t* client) {
fishsoup_packet_t pkt;
pkt.data = "Hello world!";
pkt.header.length = strlen(pkt.data) + 1;
pkt.header.flag = 0;
pkt.header.type = FISHSOUP_PACKET_USER;
fishsoup_packet_send(client->fd, &pkt);
}
int main() {
fishsoup_server_t* server = fishsoup_server(0);
fishsoup_server_on_client(server, on);
fishsoup_server_loop(server);
}