At the time of this writing, I'm working on Monster RPG 2 1.11, and one of the new things will be shipping with "regular" quality music, but giving the user the option of downloading the full quality soundtrack. Obviously this involves transferring some files over a network. I've written a simple FTP client in Java before, and I decided that's too complex for this purpose. I've decided to go with TFTP (Trivial File Transfer Protocol) instead. By "trivial" they mean very easy to implement, and it is. So what I've written is a bunch of functions that work together to do the following:
- Download a list of files to be downloaded
- Process the list and download each file listed in it
That's the gist of it. I've only tested it on OS X thus far. I want it to be fairly reliable so I do a lot of error checking. The only thing I haven't done yet is an MD5 sum. Instead, the code just checks file sizes. If this turns out to be a problem during testing (or, God forbid, after deployment), I'll have to implement something better. I'm going to post the code here now. It's plain C and does use Allegro 5, however the Allegro parts could fairly easily be removed. The only thing from Allegro needed here is the file size function, al_fsize.
Another note, this code is set up to stop downloading when the "stop" boolean variable is set to true. As a test, Allegro is used to spawn a thread and set stop to true after 30 seconds. You'll want to remove that and use "stop" more appropriately if you modify this source.
Finally, this test program will download the whole Monster RPG 2 soundtrack. TFTP is comparatively slow so expect it to take 10-15 minutes on a good connection if you let it complete.
#include <allegro5/allegro.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef WINPC
#include <windows.h>
#include <winsock2.h>
#define my_shutdown(a, b) closesocket(a)
#define SHUT_RDWR 0xf008a7
#else
#include <sys/socket.h>
#include <netdb.h>
#define INVALID_SOCKET -1
#define my_shutdown shutdown
#endif
#include <sys/time.h>
#define SERVER "nooskewl.com"
#define PORT "69"
#define BLKSIZE 32768
#define BLKSIZE_S "32768"
#define NUM_FILES 42 // FIXME: change this as needed
#define EXPECTED_LIST_SIZE (NUM_FILES * 81) // 60 chars filename 20 chars length + 1 \n
#define DOWNLOAD_PATH "." // FIXME: change me
static volatile bool stop = false;
static int sock = -1;
static struct sockaddr saddr;
static socklen_t saddr_len;
static int get(char *buf, int len)
{
int n = recvfrom(sock, buf, len, 0, &saddr, &saddr_len);
if (n < 1) return 0;
return n;
}
static int put(const char *buf, int len)
{
int n = sendto(sock, buf, len, 0, &saddr, saddr_len);
return n;
}
static bool connect_to_server(void)
{
#ifdef WINPC
WSAData crap;
WSAStartup(2, &crap);
#endif
struct addrinfo hints, *res;
// first, load up address structs with getaddrinfo():
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_DGRAM;
hints.ai_protocol = IPPROTO_UDP;
int r;
if ((r = getaddrinfo(SERVER, PORT, &hints, &res)) != 0) {
return false;
}
// make a socket:
sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (sock == INVALID_SOCKET) {
freeaddrinfo(res);
return false;
}
saddr_len = res->ai_addrlen;
memcpy(&saddr, res->ai_addr, sizeof(struct sockaddr));
freeaddrinfo(res);
return true;
}
static void shutdown_connection(void)
{
shutdown(sock, SHUT_RDWR);
}
// return size
static int get_rrq(char *buf, const char *filename)
{
buf[0] = 0;
buf[1] = 1;
buf += 2;
strcpy(buf, filename);
buf[strlen(filename)] = 0;
buf += strlen(filename)+1;
strcpy(buf, "octet");
buf[strlen("octet")] = 0;
buf += strlen("octet")+1;
strcpy(buf, "blksize");
buf[strlen("blksize")] = 0;
buf += strlen("blksize")+1;
char *bsize = BLKSIZE_S;
strcpy(buf, bsize);
buf[strlen(bsize)] = 0;
return strlen(filename) + strlen("octet") + strlen("blksize") + strlen(bsize) + 6;
}
static void mkack(char *buf)
{
buf[0] = 0;
buf[1] = 4;
}
int get16bits(const char *buf)
{
unsigned char *b1 = (unsigned char *)buf;
unsigned char *b2 = (unsigned char *)buf+1;
return ((int)(*b1) << 8) | (int)(*b2);
}
static int download_file(const char *filename)
{
char fn[1000];
sprintf(fn, "%s/%s", DOWNLOAD_PATH, filename);
FILE *f = fopen(filename, "wb"); // FIXME
char buf[4+BLKSIZE];
int sz, total_size = 0;
int last_blocknum = -1;
if (!connect_to_server()) {
printf("couldn't connect\n");
fclose(f);
return -1;
}
sz = get_rrq(buf, filename);
if (put(buf, sz) != sz) {
printf("read request fail\n");
fclose(f);
shutdown_connection();
return -1;
}
// read option acknowledgement
sz = get(buf, 2+strlen("blksize")+1+strlen(BLKSIZE_S)+1);
if ((sz != (2+strlen("blksize")+1+strlen(BLKSIZE_S)+1)) || get16bits(buf) != 6) {
fclose(f);
shutdown_connection();
return -1;
}
buf[0] = 0;
buf[1] = 4;
buf[2] = 0;
buf[3] = 0;
if (put(buf, 4) != 4) {
fclose(f);
shutdown_connection();
return -1;
}
printf("downloading %s\n", filename);
while (1) {
if (stop) {
fclose(f);
shutdown_connection();
return -1;
}
fd_set fdset;
FD_ZERO(&fdset);
FD_SET(sock, &fdset);
struct timeval tv;
tv.tv_sec = 60;
tv.tv_usec = 0;
if (select(sock+1, &fdset, 0, 0, &tv) == 0) {
fclose(f);
shutdown_connection();
return -1;
}
sz = get(buf, 4+BLKSIZE);
if (sz != 4+BLKSIZE) {
break;
}
int blocknum = get16bits(buf+2);
if (last_blocknum == -1 || blocknum != last_blocknum) {
fwrite(buf+4, BLKSIZE, 1, f);
total_size += BLKSIZE;
}
last_blocknum = blocknum;
mkack(buf);
if (put(buf, 4) != 4) {
fclose(f);
shutdown_connection();
return -1;
}
}
if (sz > 4) {
fwrite(buf+4, sz-4, 1, f);
total_size += sz - 4;
mkack(buf);
if (put(buf, 4) != 4) {
fclose(f);
shutdown_connection();
return -1;
}
}
fclose(f);
shutdown_connection();
return total_size;
}
void download_list(char **filenames, int *lengths)
{
int i = 0;
while (filenames[i]) {
int len = download_file(filenames[i]);
if (stop) {
return;
}
if (len != lengths[i])
continue;
i++;
}
}
bool download_all(void)
{
char fn[1000];
sprintf(fn, "%s/%s", DOWNLOAD_PATH, "list.txt");
FILE *f = fopen(fn, "r");
size_t read;
char buf[100];
int sz;
char **filenames = NULL;
int *lengths = NULL;
int count = 0;
while ((read = fread(buf, 80, 1, f)) == 1) {
fseek(f, 1, SEEK_CUR);
buf[80] = 0;
if (sscanf(buf, "%s %d", fn, &sz) != 2) {
fclose(f);
return false;
}
ALLEGRO_FILE *f2 = al_fopen(fn, "rb");
if (f2) {
int sz2 = al_fsize(f2);
al_fclose(f2);
if (sz2 == sz) {
continue;
}
}
count++;
if (filenames == NULL) {
filenames = malloc(2 * sizeof(char *));
lengths = malloc(2 * sizeof(int));
}
else {
filenames = realloc(filenames, (count+1) * sizeof(char *));
lengths = realloc(lengths, (count+1) * sizeof(int));
}
filenames[count-1] = strdup(fn);
lengths[count-1] = sz;
}
if (filenames != NULL) {
filenames[count] = NULL;
lengths[count] = 0;
printf("Downloading %d files\n", count);
download_list(filenames, lengths);
free(filenames);
free(lengths);
}
else {
printf("Already done!\n");
}
fclose(f);
return true;
}
static const char *get_status(float *percent)
{
*percent = 0.0f;
char fn[1000];
sprintf(fn, "%s/%s", DOWNLOAD_PATH, "list.txt");
ALLEGRO_FILE *f = al_fopen(fn, "r");
if (f == NULL || al_fsize(f) != EXPECTED_LIST_SIZE) {
if (f) al_fclose(f);
goto nuthin;
}
size_t read;
char buf[100];
int sz;
int count = 0;
while ((read = al_fread(f, buf, 80)) == 80) {
al_fseek(f, 1, ALLEGRO_SEEK_CUR);
buf[80] = 0;
if (sscanf(buf, "%s %d", fn, &sz) != 2) {
if (count == 0) {
al_fclose(f);
goto nuthin;
}
al_fclose(f);
goto partial;
}
ALLEGRO_FILE *f2 = al_fopen(fn, "rb");
if (f2) {
int sz2 = al_fsize(f2);
al_fclose(f2);
if (sz2 != sz) {
if (count == 0) {
al_fclose(f);
goto nuthin;
}
goto partial;
}
}
else {
if (count == 0) {
al_fclose(f);
goto nuthin;
}
goto partial;
}
count++;
}
if (count == NUM_FILES) {
al_fclose(f);
*percent = 1.0f;
return "Complete";
}
partial:
al_fclose(f);
*percent = (float)count / NUM_FILES;
return "Partially downloaded";
nuthin:
return "Nothing downloaded";
}
void *test_thread(void *arg)
{
(void)arg;
al_rest(30);
stop = true;
return NULL;
}
int main(void)
{
al_init();
al_run_detached_thread(test_thread, NULL);
int len = download_file("list.txt");
if (len != EXPECTED_LIST_SIZE) {
printf("Connection error\n");
return 1;
}
const char *status;
float percent;
status = get_status(&percent);
printf("Status: %s (%d percent)\n", status, (int)(100*percent));
bool ret = download_all();
if (ret) printf("Success!\n");
else printf("Error!\n");
return 0;
}