diff --git a/.gitignore b/.gitignore index 259148f..477abb0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,32 +1,149 @@ -# Prerequisites -*.d - -# Compiled Object files -*.slo -*.lo -*.o -*.obj - -# Precompiled Headers -*.gch -*.pch - -# Compiled Dynamic libraries -*.so -*.dylib -*.dll - -# Fortran module files -*.mod -*.smod - -# Compiled Static libraries -*.lai -*.la -*.a -*.lib - -# Executables -*.exe -*.out -*.app +# Created by https://www.gitignore.io/api/java,macos,gradle,intellij + +### Intellij ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +### Intellij Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +.idea/sonarlint + +### Java ### +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Gradle ### +.gradle +/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache + +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +# gradle/wrapper/gradle-wrapper.properties + + +# End of https://www.gitignore.io/api/java,macos,gradle,intellij \ No newline at end of file diff --git a/README.md b/README.md index a5497fb..ae4080b 100644 --- a/README.md +++ b/README.md @@ -1 +1,57 @@ -# NetworksLab2017 \ No newline at end of file +### Сервер +```sh +java -jar server/build/libs/TorrentServer-1.0-SNAPSHOT.jar +``` + +### Клиент +```sh +java -jar client/build/libs/TorrentClient-1.0-SNAPSHOT.jar +``` + +### Torrent +* На трекере хранится список файлов и информация об активных пользователях, у которых есть те или иные файлы (возможно не целиком). +* С помощью клиентского приложения можно просматривать список файлов на трекере, а также добавлять новые и выбирать файлы из списка для скачивания. +* Файлы условно разбиваются на последовательные блоки бинарных данных константного размера (10Mб) +* Клиент при подключении отправляет на трекер список раздаваемых им файлов. +* При скачивании файла клиент получает у трекера информацию о клиентах, раздающих файл (сидах), и далее общается с ними напрямую. +* У отдельного сида можно узнать о том, какие полные части у него есть, а также скачать их. +* После скачивания отдельных блоков некоторого файла клиент становится сидом. + +### Torrent-tracker +* Хранит мета-информацию о раздаваемых файлах: + * идентификатор + * активные клиенты (недавно был update), у которых есть этот файл целиком или некоторые его части +* Порт сервера: 8081 +* Запросы: + * list — список раздаваемых файлов + * upload — публикация нового файла + * sources — список клиентов, владеющих определенным файлов целиком или некоторыми его частями + * update — загрузка клиентом данных о раздаваемых файлах + +#### List +Формат запроса: <1: Byte> Формат ответа: ( )*, count — количество файлов id — идентификатор файла name — название файла size — размер файла + +#### Upload +Формат запроса: <2: Byte> , name — название файла size — размер файла Формат ответа: , id — идентификатор файла + +#### Sources +Формат запроса: <3: Byte> , id — идентификатор файла Формат ответа: ( )*, size — количество клиентов, раздающих файл ip — ip клиента + +#### Update +Формат запроса: <4: Byte> ()*, clientPort — порт клиента, count — количество раздаваемых файлов, id — идентификатор файла Формат ответа: , status — True, если информация успешно обновлена + +Клиент обязан исполнять данный запрос каждые 5 минут, иначе сервер считает, что клиент ушел с раздачи + +### Torrent-client +* Порт клиента указывается при запуске и передается на трекер в рамках запроса update +* Каждый файл раздается по частям, размер части — константа на всё приложение +* Клиент хранит и раздает эти самые части +* Запросы: + * stat — доступные для раздачи части определенного файла + * get — скачивание части определенного файла + +#### Stat +Формат запроса: <1: Byte> , id — идентификатор файла Формат ответа: ()*, count — количество доступных частей part — номер части + +#### Get +Формат запроса: <2: Byte> id — идентификатор файла, part — номер части Формат ответа: \ No newline at end of file diff --git a/client/build.gradle b/client/build.gradle new file mode 100644 index 0000000..0188e09 --- /dev/null +++ b/client/build.gradle @@ -0,0 +1,28 @@ +plugins { + id 'java' +} + +group 'itmo2018.se' +version '1.0-SNAPSHOT' + +sourceCompatibility = 1.8 + +repositories { + mavenCentral() +} + +jar { + manifest { + attributes 'Main-Class': 'itmo2018.se.ClientMain' + } + + from { + configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } + } +} + +dependencies { + testCompile group: 'junit', name: 'junit', version: '4.12' + compile group: 'org.apache.commons', name: 'commons-io', version: '1.3.2' + compile group: 'org.apache.commons', name: 'commons-collections4', version: '4.2' +} diff --git a/client/build/libs/TorrentClient-1.0-SNAPSHOT.jar b/client/build/libs/TorrentClient-1.0-SNAPSHOT.jar new file mode 100644 index 0000000..b697533 Binary files /dev/null and b/client/build/libs/TorrentClient-1.0-SNAPSHOT.jar differ diff --git a/client/gradle/wrapper/gradle-wrapper.jar b/client/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..1948b90 Binary files /dev/null and b/client/gradle/wrapper/gradle-wrapper.jar differ diff --git a/client/gradle/wrapper/gradle-wrapper.properties b/client/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..d2c45a4 --- /dev/null +++ b/client/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.8-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/client/gradlew b/client/gradlew new file mode 100755 index 0000000..cccdd3d --- /dev/null +++ b/client/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/client/gradlew.bat b/client/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/client/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/client/settings.gradle b/client/settings.gradle new file mode 100644 index 0000000..7794e2a --- /dev/null +++ b/client/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'TorrentClient' + diff --git a/client/src/main/java/itmo2018/se/Client.java b/client/src/main/java/itmo2018/se/Client.java new file mode 100644 index 0000000..5985649 --- /dev/null +++ b/client/src/main/java/itmo2018/se/Client.java @@ -0,0 +1,169 @@ +package itmo2018.se; + +import java.io.*; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class Client implements Closeable { + private MetaDataManager metaData; + private Socket socket; + private DataOutputStream socketOut; + private DataInputStream socketIn; + private ScheduledExecutorService scheduled; + + //"192.168.1.2" + public Client(String host, MetaDataManager metaData) throws IOException { + this.socket = new Socket(host, 8081); + this.metaData = metaData; + try { + socketOut = new DataOutputStream(socket.getOutputStream()); + socketIn = new DataInputStream(socket.getInputStream()); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public List getSources(int id) throws IOException { + synchronized (socketOut) { + socketOut.writeInt(1 + 4); + socketOut.writeByte(3); + socketOut.writeInt(id); + + int size = socketIn.readInt(); + List res = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + int ip1 = byteToInt(socketIn.readByte()); + int ip2 = byteToInt(socketIn.readByte()); + int ip3 = byteToInt(socketIn.readByte()); + int ip4 = byteToInt(socketIn.readByte()); + int port = shortToInt(socketIn.readShort()); + InetSocketAddress address = new InetSocketAddress(ip1 + "." + ip2 + "." + ip3 + "." + ip4, port); + res.add(address); + } + return res; + } + } + + public void printSource(int id) throws IOException { + List adresses = getSources(id); + for (InetSocketAddress address : adresses) { + System.out.println(address); + } + } + + public void printList() throws IOException { + List notes = getList(); + for (MetaDataNote note : notes) { + System.out.println(note.getId() + "\t" + note.getName() + "\t" + note.getSize()); + } + } + + public List getList() throws IOException { + synchronized (socketOut) { + socketOut.writeInt(1); + socketOut.writeByte(1); + + int count = socketIn.readInt(); + List result = new ArrayList<>(); + for (int i = 0; i < count; i++) { + int fileId = socketIn.readInt(); + String fileName = socketIn.readUTF(); + long fileSize = socketIn.readLong(); + result.add(new MetaDataNote(fileId, fileName, fileSize)); + } + return result; + } + } + + public void upload(String filePath) throws IOException { + File file = new File(filePath); + if (!file.exists() || file.isDirectory()) { + System.out.println("can't find file"); + return; + } + String name = file.getName(); + long size = file.length(); + synchronized (socketOut) { + socketOut.writeInt(1 + name.getBytes().length + 2 + 8); + socketOut.writeByte(2); + socketOut.writeUTF(name); + socketOut.writeLong(size); + socketOut.flush(); + + int id = socketIn.readInt(); + metaData.addReadyNote(id, file.getAbsolutePath(), file.length()); + System.out.println("new file id: " + id); + } + } + + public void update(short seederPort) throws IOException { + synchronized (socketOut) { + int filesCount = metaData.filesCount(); + socketOut.writeInt(1 + 2 + (filesCount + 1) * 4); + socketOut.writeByte(4); + socketOut.writeShort(seederPort); + socketOut.writeInt(filesCount); + for (int id : metaData.idSet()) { + socketOut.writeInt(id); + } + socketOut.flush(); + socketIn.readByte(); + } + } + + public void startUpdat(short seedPort) { + Updater updater = new Updater(seedPort); + this.scheduled = Executors.newSingleThreadScheduledExecutor(); + scheduled.scheduleAtFixedRate(updater, 0, 5, TimeUnit.MINUTES); + } + + public SocketAddress getSocketLocalAdress() { + return socket.getLocalSocketAddress(); + } + + + @Override + public void close() throws IOException { + socket.close(); + if (scheduled != null) { + scheduled.shutdownNow(); + } + } + + private int byteToInt(byte b) { + if (b >= 0) { + return b; + } + return 128 + 128 + b; + } + + private int shortToInt(short s) { + if (s >= 0) { + return s; + } + return 32768 + 32768 + s; + } + + private class Updater implements Runnable { + short seederPort; + + Updater(short seederPort) { + this.seederPort = seederPort; + } + + @Override + public void run() { + try { + update(seederPort); + } catch (IOException e) { + e.printStackTrace(); + } + } + } +} diff --git a/client/src/main/java/itmo2018/se/ClientMain.java b/client/src/main/java/itmo2018/se/ClientMain.java new file mode 100644 index 0000000..a2cb9d4 --- /dev/null +++ b/client/src/main/java/itmo2018/se/ClientMain.java @@ -0,0 +1,133 @@ +package itmo2018.se; + +import java.io.*; +import java.net.ServerSocket; +import java.util.Scanner; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class ClientMain { + public static void main(String[] args) throws IOException { + File workingDir; + if (args.length == 0) { + System.out.println("lack of tracker host and working directory"); + return; + } else if (args.length == 1) { + workingDir = new File(System.getProperty("user.dir")); + } else { + workingDir = new File(args[1]); + if (!workingDir.exists() && !workingDir.isDirectory()) { + System.out.println("can't find " + args[1] + " folder"); + return; + } + } + String host = args[0]; + new ClientMain().run(host, workingDir.getAbsolutePath()); + } + + public void run(String trackerHost, String workingDir) throws IOException { + MetaDataManager metaData = initWorkingDir(workingDir); + + Client client; + try { + client = new Client(trackerHost, metaData); + } catch (IOException e) { + System.out.println("can't connect to server"); + return; + } + ServerSocket seederServer = new ServerSocket(); + seederServer.bind(client.getSocketLocalAdress()); + seederServer.setReceiveBufferSize(1024); + + short seedPort = (short) seederServer.getLocalPort(); + client.startUpdat(seedPort); + + Seeder seeder = new Seeder(seederServer, metaData, client); + Thread seederThread = new Thread(seeder); + seederThread.setDaemon(true); + seederThread.start(); + + ExecutorService leechPool = Executors.newFixedThreadPool(4); + + Scanner scanner = new Scanner(System.in); + while (true) { + String[] cmdLine = scanner.nextLine().split(" +"); + try { + switch (cmdLine[0]) { + case "download": + if (cmdLine.length != 2) { + System.out.println("download takes one argument"); + continue; + } else if (!isPositiveInt(cmdLine[1])) { + System.out.println("id must be positive integer number"); + continue; + } + leechPool.submit(new Leech(Integer.parseInt(cmdLine[1]), client, metaData, seedPort)); + break; + case "list": + if (cmdLine.length != 1) { + System.out.println("list not takes arguments"); + continue; + } + client.printList(); + break; + case "upload": + if (cmdLine.length != 2) { + System.out.println("upload takes one argument"); + continue; + } + client.upload(cmdLine[1]); + break; + case "source": + if (cmdLine.length != 2) { + System.out.println("source takes one argument"); + continue; + } else if (!isPositiveInt(cmdLine[1])) { + System.out.println("id must be positive integer number"); + continue; + } + client.printSource(Integer.parseInt(cmdLine[1])); + break; + case "update": + if (cmdLine.length != 1) { + System.out.println("update not takes arguments"); + continue; + } + client.update(seedPort); + break; + case "exit": + if (cmdLine.length != 1) { + System.out.println("exit not takes arguments"); + continue; + } + client.close(); + seeder.close(); + leechPool.shutdownNow(); + return; + default: + System.out.println(cmdLine[0] + " is unknow command"); + } + } catch (IOException e) { + System.out.println("connection aborted"); + client.close(); + seeder.close(); + leechPool.shutdown(); + return; + } + } + } + + private MetaDataManager initWorkingDir(String workingDir) throws IOException { + System.out.println("working dir:\t" + workingDir); + File metaData = new File(workingDir + "/.metadata"); + if (!metaData.exists()) { + metaData.createNewFile(); + } + return new MetaDataManager(metaData); + } + + private boolean isPositiveInt(String str) { + return str.matches("\\d+"); + } +} + diff --git a/client/src/main/java/itmo2018/se/Leech.java b/client/src/main/java/itmo2018/se/Leech.java new file mode 100644 index 0000000..bf8f3d6 --- /dev/null +++ b/client/src/main/java/itmo2018/se/Leech.java @@ -0,0 +1,237 @@ +package itmo2018.se; + +import org.apache.commons.collections4.SetUtils; + +import java.io.*; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.*; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class Leech implements Runnable { + private MetaDataManager metaData; + private int fileId; + private Client client; + private ThreadPoolExecutor downloadPool = (ThreadPoolExecutor) Executors.newFixedThreadPool(8); + private final Object mutex = new Object(); + private short ownSeederPort; + private Set usedSeeders = Collections.synchronizedSet(new HashSet<>()); + + public Leech(int fileId, Client client, MetaDataManager metaData, short ownSeederPort) { + this.fileId = fileId; + this.client = client; + this.metaData = metaData; + this.ownSeederPort = ownSeederPort; + } + + @Override + public void run() { + FileWriter writer = null; + try { + MetaDataNote note = metaData.getNote(fileId); + if (note != null && !note.existFile()) { + metaData.deleteNote(fileId); + note = null; + } + List owners = client.getSources(fileId); + if (owners.size() == 0) { + System.out.println("no active seeders, please try later"); + return; + } + + MetaDataNote fileInfo = client.getList().get(fileId); + Set parts; + if (note == null) { + metaData.addNote(fileInfo.getId(), fileInfo.getName(), fileInfo.getSize()); + parts = new HashSet<>(); + } else { + parts = note.getParts(); + } + + Map neededParts = SetUtils.difference(fileInfo.getParts(), parts) + .stream().collect(Collectors.toConcurrentMap(Function.identity(), + it -> new PartHolder(it, DownloadStatus.WAITING))); + + writer = new FileWriter(fileInfo); + writer.start(); + + int finish; + do { + for (InetSocketAddress owner : owners) { + if (downloadPool.getActiveCount() < downloadPool.getMaximumPoolSize()) { + if (!usedSeeders.contains(owner)) { + downloadPool.submit(new Downloader(owner, fileInfo, neededParts, writer)); + } + } + } + Thread.sleep(20000); + finish = (int) neededParts.values().stream().filter(it -> it.status == DownloadStatus.FINISH).count(); + owners = client.getSources(fileId); + } while (downloadPool.getActiveCount() > 0 && finish < neededParts.size()); + + if (finish == neededParts.size()) { + metaData.finishCollectParts(fileId); + System.out.println("finish download file " + fileInfo.getName()); + } else { + System.out.println("not possible to download the whole file because there are no active seeders"); + } + client.update(ownSeederPort); + } catch (IOException e) { + System.out.println("can't connect to seeder"); + } catch (InterruptedException e) { + //interupt + } finally { + downloadPool.shutdownNow(); + if (writer != null) { + writer.interrupt(); + } + } + } + + private class Downloader implements Runnable { + InetSocketAddress address; + final Map neededParts; + MetaDataNote fileInfo; + FileWriter writer; + + Downloader(InetSocketAddress address, MetaDataNote fileInfo, Map neededParts, + FileWriter writer) { + this.address = address; + this.neededParts = neededParts; + this.fileInfo = fileInfo; + this.writer = writer; + } + + @Override + public void run() { + try (Socket socket = new Socket()) { + socket.connect(address); + Set userParts = getStat(socket); + usedSeeders.add(address); + + for (Map.Entry neededPart : neededParts.entrySet()) { + int partNumber = neededPart.getKey(); + if (!userParts.contains(partNumber)) { + continue; + } + PartHolder partHolder = neededPart.getValue(); + if (partHolder.status == DownloadStatus.WAITING) { + synchronized (neededParts) { + partHolder = neededParts.get(partNumber); + if (partHolder.status == DownloadStatus.WAITING) { + partHolder.status = DownloadStatus.IN_PROGRESS; + } else { + continue; + } + } + try { + partHolder.content = download(socket, partNumber); + writer.addPart(partHolder); + } catch (Exception e) { + partHolder.status = DownloadStatus.WAITING; + return; + } + } + } + } catch (IOException e) { + //seeder is busy + } finally { + usedSeeders.remove(address); + synchronized (mutex) { + mutex.notify(); + } + } + } + + private byte[] download(Socket socket, int part) throws IOException { + DataInputStream socketIn = new DataInputStream(socket.getInputStream()); + DataOutputStream socketOut = new DataOutputStream(socket.getOutputStream()); + socketOut.writeByte(2); + socketOut.writeInt(fileInfo.getId()); + socketOut.writeInt(part); + socketOut.flush(); + + int partSize = socketIn.readInt(); + byte[] bytes = new byte[partSize]; + for (int i = 0; i < partSize; i++) { + bytes[i] = socketIn.readByte(); + } + return bytes; + } + + private Set getStat(Socket socket) throws IOException { + DataInputStream socketIn = new DataInputStream(socket.getInputStream()); + DataOutputStream socketOut = new DataOutputStream(socket.getOutputStream()); + socketOut.writeByte(1); + socketOut.writeInt(fileId); + socketOut.flush(); + + int partsCount = socketIn.readInt(); + Set result = new HashSet<>(); + for (int i = 0; i < partsCount; i++) { + int part = socketIn.readInt(); + result.add(part); + } + return result; + } + } + + private class FileWriter extends Thread { + MetaDataNote fileInfo; + BlockingQueue queue = new LinkedBlockingQueue<>(); + + FileWriter(MetaDataNote fileInfo) { + this.fileInfo = fileInfo; + } + + void addPart(PartHolder partHolder) throws InterruptedException { + queue.put(partHolder); + } + + @Override + public void run() { + int partSize = 1024 * 1024 * 5; + File file = new File(fileInfo.getName()); + if (!file.exists()) { + try { + file.createNewFile(); + } catch (IOException e) { + System.out.println("file could not be created"); + } + } + try (RandomAccessFile accessfile = new RandomAccessFile(file, "rw")) { + while (!this.isInterrupted() || queue.size() > 0) { + PartHolder partHolder = queue.take(); + accessfile.seek(partSize * partHolder.number); + accessfile.write(partHolder.content); + metaData.addPart(fileInfo.getId(), partHolder.number); + partHolder.status = DownloadStatus.FINISH; + } + } catch (IOException e) { + e.printStackTrace(); + } catch (InterruptedException e) { + //return from writer + } + } + } + + private enum DownloadStatus { + WAITING, IN_PROGRESS, FINISH + } + + private class PartHolder { + int number; + volatile byte[] content; + volatile DownloadStatus status; + + PartHolder(int number, DownloadStatus status) { + this.number = number; + this.status = status; + } + } +} diff --git a/client/src/main/java/itmo2018/se/MetaDataManager.java b/client/src/main/java/itmo2018/se/MetaDataManager.java new file mode 100644 index 0000000..1adc510 --- /dev/null +++ b/client/src/main/java/itmo2018/se/MetaDataManager.java @@ -0,0 +1,137 @@ +package itmo2018.se; + +import java.io.*; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +public class MetaDataManager { + private File file; + private int filesCount; + + public MetaDataManager(File file) throws IOException { + this.file = file; + if (!file.exists() || file.isDirectory()) { + throw new FileNotFoundException(); + } + this.filesCount = (int) Files.lines(file.toPath()).count(); + } + + public void addReadyNote(int id, String name, long size) throws IOException { + try (OutputStream writer = new FileOutputStream(file, true)) { + writer.write((id + "\t" + name + "\t" + size + "\t" + -1 + "\n").getBytes()); + } + filesCount++; + } + + public void addNote(int id, String name, long size) throws IOException { + try (OutputStream writer = new FileOutputStream(file, true)) { + writer.write((id + "\t" + name + "\t" + size + "\n").getBytes()); + } + filesCount++; + } + + public void addNote(int id, String name, long size, Set parts) throws IOException { + long partSize = 1024L * 1024L * 5; + int partsCount = (int) ((size - 1) / partSize + 1); + if (parts.size() == partsCount) { + addReadyNote(id, name, size); + } else { + try (OutputStream writer = new FileOutputStream(file, true)) { + writer.write((id + "\t" + name + "\t" + size).getBytes()); + for (int part : parts) { + writer.write(("\t" + part).getBytes()); + } + writer.write("\n".getBytes()); + } + } + filesCount++; + } + + public void addPart(int id, int part) throws IOException { + List lines = Files.lines(file.toPath()).collect(Collectors.toList()); + try (OutputStream writer = new FileOutputStream(file, false)) { + for (String line : lines) { + if (line.startsWith(id + "\t")) { + String[] data = line.split("\t"); + if (data.length == 3) { + writer.write((line + "\t" + part + "\n").getBytes()); + } else if (!data[3].equals("-1")) { + writer.write((line + "," + part + "\n").getBytes()); + } + } else { + writer.write((line + "\n").getBytes()); + } + } + } + } + + public void deleteNote(int id) throws IOException { + List lines = Files.lines(file.toPath()).collect(Collectors.toList()); + try (OutputStream writer = new FileOutputStream(file, false)) { + for (String line : lines) { + if (!line.startsWith(id + "\t")) { + writer.write((line + "\n").getBytes()); + } else { + filesCount--; + } + } + } + } + + public MetaDataNote getNote(int id) throws IOException { + Optional optionalfileDescription = Files.lines(file.toPath()) + .filter(it -> it.startsWith(id + "\t")) + .map(it -> it.split("\t")) + .findFirst(); + if (!optionalfileDescription.isPresent()) { + return null; + } + String[] fileDescription = optionalfileDescription.get(); + if (fileDescription[3].equals("-1")) { + return new MetaDataNote(Integer.parseInt(fileDescription[0]), + fileDescription[1], Long.parseLong(fileDescription[2])); + } + Set parts = Arrays.stream(fileDescription[3].split(",")) + .map(Integer::parseInt).collect(Collectors.toSet()); + MetaDataNote result = new MetaDataNote(Integer.parseInt(fileDescription[0]), + fileDescription[1], Long.parseLong(fileDescription[2]), parts); + if (result.isFinish()) { + finishCollectParts(id); + } + return result; + } + + public int filesCount() { + return filesCount; + } + + public Set idSet() throws IOException { + return Files.lines(file.toPath()).map(it -> Integer.parseInt(it.split("\t")[0])) + .collect(Collectors.toSet()); + } + + public void finishCollectParts(int id) throws IOException { + List lines = Files.lines(file.toPath()).collect(Collectors.toList()); + try (OutputStream writer = new FileOutputStream(file, false)) { + for (String line : lines) { + if (line.startsWith(id + "\t")) { + String[] data = line.split("\t"); + writer.write((data[0] + "\t" + data[1] + "\t" + data[2] + "\t" + -1 + "\n").getBytes()); + } else { + writer.write((line + "\n").getBytes()); + } + } + } + } + +// private void isExist(int id) throws IOException { +// if (Files.lines(file.toPath()) +// .anyMatch(it -> it.startsWith(id + "\t"))) { +// throw new IOException("file with " + id + " id already exist"); +// } +// } +} diff --git a/client/src/main/java/itmo2018/se/MetaDataNote.java b/client/src/main/java/itmo2018/se/MetaDataNote.java new file mode 100644 index 0000000..a4e6dde --- /dev/null +++ b/client/src/main/java/itmo2018/se/MetaDataNote.java @@ -0,0 +1,76 @@ +package itmo2018.se; + +import java.io.File; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +public class MetaDataNote implements Iterable { + private int id; + private String name; + private long size; + private Set parts; + private boolean finish = false; + + public MetaDataNote(int id, String name, long size) { + this.id = id; + this.name = name; + this.size = size; + this.finish = true; + } + + public MetaDataNote(int id, String name, long size, Set parts) { + this.id = id; + this.name = name; + this.size = size; + this.parts = parts; + } + + public int getId() { + return id; + } + + public String getName() { + return name; + } + + public long getSize() { + return size; + } + + public Set getParts() { + if (parts == null) { + initParts(); + } + return parts; + } + + public int partsCount() { + if (parts == null) { + initParts(); + } + return parts.size(); + } + + public boolean isFinish() { + return finish; + } + + public boolean existFile() { + return new File(name).exists(); + } + + @Override + public Iterator iterator() { + return parts.iterator(); + } + + private void initParts() { + this.parts = new HashSet<>(); + int partSize = 1024 * 1024 * 5; + int partsCount = (int) ((size - 1) / partSize + 1); + for (int i = 0; i < partsCount; i++) { + parts.add(i); + } + } +} diff --git a/client/src/main/java/itmo2018/se/Seeder.java b/client/src/main/java/itmo2018/se/Seeder.java new file mode 100644 index 0000000..6ac424b --- /dev/null +++ b/client/src/main/java/itmo2018/se/Seeder.java @@ -0,0 +1,128 @@ +package itmo2018.se; + +import itmo2018.se.SingletonFileReader.FileHolder; + +import java.io.*; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadPoolExecutor; + +public class Seeder implements Runnable, Closeable { + private ServerSocket server; + private MetaDataManager metaData; + private ThreadPoolExecutor poolExecutor; + private Client client; + private int limitLeech = 4; + + public Seeder(ServerSocket server, MetaDataManager metaData, Client client) { + this.server = server; + this.metaData = metaData; + this.poolExecutor = (ThreadPoolExecutor) Executors.newFixedThreadPool(limitLeech); + this.client = client; + } + + @Override + public void run() { + while (true) { + try { + Socket socket = server.accept(); + if (poolExecutor.getActiveCount() >= limitLeech) { + socket.close(); + continue; + } + poolExecutor.submit(new Executor(socket)); + } catch (IOException e) { + break; + } + } + } + + @Override + public void close() throws IOException { + server.close(); + poolExecutor.shutdownNow(); + } + + private class Executor implements Runnable { + Socket socket; + FileHolder file = null; + + Executor(Socket socket) { + this.socket = socket; + } + + @Override + public void run() { + while (true) { + try (DataInputStream socketIn = new DataInputStream(socket.getInputStream()); + DataOutputStream socketOut = new DataOutputStream(socket.getOutputStream())) { + while (true) { + byte cmd = socketIn.readByte(); + if (cmd == 1) { + execStat(socketIn, socketOut); + } else { + execGet(socketIn, socketOut); + } + } + } catch (IOException e) { + //disconnect leech + } + try { + socket.close(); + if (file != null) { + file.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + return; + } + } + + private void execStat(DataInputStream socketIn, DataOutputStream socketOut) throws IOException { + int id = socketIn.readInt(); + MetaDataNote note = metaData.getNote(id); + if (note == null) { + socketOut.writeInt(0); + } else if (!note.existFile()) { + metaData.deleteNote(id); + socketOut.writeInt(0); + } else { + socketOut.writeInt(note.partsCount()); + for (int part : note) { + socketOut.writeInt(part); + } + } + socketOut.flush(); + } + + private void execGet(DataInputStream socketIn, DataOutputStream socketOut) throws IOException { + int id = socketIn.readInt(); + int part = socketIn.readInt(); + + MetaDataNote note = metaData.getNote(id); + if (file == null) { + File neededFile = new File(note.getName()); + if (!neededFile.exists()) { + metaData.deleteNote(id); + client.update((short) server.getLocalPort()); + socketOut.writeInt(0); + socketOut.flush(); + socket.close(); + return; + } + file = SingletonFileReader.getFile(neededFile); + } + long defaultPartSize = 1024 * 1024 * 5; + int partSize = (int) (file.length() - defaultPartSize * part > defaultPartSize ? + defaultPartSize : file.length() - defaultPartSize * part); + byte[] bytes = new byte[partSize]; + file.read(defaultPartSize * part, bytes); + + socketOut.writeInt(partSize); + socketOut.write(bytes); + socketOut.flush(); + } + } +} diff --git a/client/src/main/java/itmo2018/se/SingletonFileReader.java b/client/src/main/java/itmo2018/se/SingletonFileReader.java new file mode 100644 index 0000000..acc9c47 --- /dev/null +++ b/client/src/main/java/itmo2018/se/SingletonFileReader.java @@ -0,0 +1,62 @@ +package itmo2018.se; + +import java.io.*; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +public class SingletonFileReader { + private static final Map files = new ConcurrentHashMap<>(); + + private SingletonFileReader() { + } + + public static FileHolder getFile(File file) throws FileNotFoundException { + String path = file.getAbsolutePath(); + if (!files.containsKey(path)) { + synchronized (files) { + if (!files.containsKey(path)) { + files.put(path, new FileHolder(path)); + } else { + files.get(path).referenceCount.incrementAndGet(); + } + } + } else { + files.get(path).referenceCount.incrementAndGet(); + } + return files.get(path); + } + + public static class FileHolder implements Closeable { + private RandomAccessFile file; + private AtomicInteger referenceCount; + private String path; + + private FileHolder(String path) throws FileNotFoundException { + this.file = new RandomAccessFile(path, "r"); + this.referenceCount = new AtomicInteger(1); + this.path = path; + } + + public synchronized void read(long offset, byte[] bytes) throws IOException { + file.seek(offset); + file.read(bytes); + } + + public long length() throws IOException { + return file.length(); + } + + public void close() throws IOException { + synchronized (files) { + FileHolder holder = files.get(path); + if (holder.referenceCount.get() == 1) { + holder.file.close(); + files.remove(path); + } else { + holder.referenceCount.decrementAndGet(); + } + } + } + } +} diff --git a/server/build.gradle b/server/build.gradle new file mode 100644 index 0000000..90cd737 --- /dev/null +++ b/server/build.gradle @@ -0,0 +1,22 @@ +plugins { + id 'java' +} + +group 'itmo2018.se' +version '1.0-SNAPSHOT' + +sourceCompatibility = 1.8 + +repositories { + mavenCentral() +} + +jar { + manifest { + attributes 'Main-Class': 'itmo2018.se.Server' + } +} + +dependencies { + testCompile group: 'junit', name: 'junit', version: '4.12' +} diff --git a/server/build/libs/TorrentServer-1.0-SNAPSHOT.jar b/server/build/libs/TorrentServer-1.0-SNAPSHOT.jar new file mode 100644 index 0000000..bcfa110 Binary files /dev/null and b/server/build/libs/TorrentServer-1.0-SNAPSHOT.jar differ diff --git a/server/gradle/wrapper/gradle-wrapper.jar b/server/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..1948b90 Binary files /dev/null and b/server/gradle/wrapper/gradle-wrapper.jar differ diff --git a/server/gradle/wrapper/gradle-wrapper.properties b/server/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..d2c45a4 --- /dev/null +++ b/server/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.8-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/server/gradlew b/server/gradlew new file mode 100755 index 0000000..cccdd3d --- /dev/null +++ b/server/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/server/gradlew.bat b/server/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/server/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/server/settings.gradle b/server/settings.gradle new file mode 100644 index 0000000..66391d0 --- /dev/null +++ b/server/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'TorrentServer' + diff --git a/server/src/main/java/itmo2018/se/ClientDataHolder.java b/server/src/main/java/itmo2018/se/ClientDataHolder.java new file mode 100644 index 0000000..5af3367 --- /dev/null +++ b/server/src/main/java/itmo2018/se/ClientDataHolder.java @@ -0,0 +1,71 @@ +package itmo2018.se; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +public class ClientDataHolder { + private byte[] request; + private Queue responseQueue = new ConcurrentLinkedQueue<>(); + private ClientInfo clientInfo; + + private List bytesOfZize = new ArrayList<>(4); + private int packageSize = -1; + private int currentSize = 0; + + public ClientDataHolder(ClientInfo clientInfo) { + this.clientInfo = clientInfo; + } + + public void read(ByteBuffer buffer) { + if (packageSize == -1) { + int intSize = 4; + while (buffer.position() < buffer.limit() && bytesOfZize.size() < intSize) { + bytesOfZize.add(buffer.get()); + } + if (bytesOfZize.size() == intSize) { + packageSize = bytesToInt(bytesOfZize); + request = new byte[packageSize]; + } + } + while (buffer.position() < buffer.limit() && currentSize < packageSize) { + request[currentSize] = buffer.get(); + currentSize++; + } + } + + public boolean requestIsReady() { + return packageSize == currentSize; + } + + public byte[] getRequest() { + return request; + } + + public void resetRequest() { + packageSize = -1; + currentSize = 0; + bytesOfZize = new ArrayList<>(4); + } + + public ClientInfo getClientInfo() { + return clientInfo; + } + + public void addResponse(ByteBuffer response) { + responseQueue.add(response); + } + + public ByteBuffer getResponse() { + return responseQueue.poll(); + } + + private int bytesToInt(List b) { + if (b.size() != 4) { + return 0; + } + return b.get(0) << 24 | (b.get(1) & 0xff) << 16 | (b.get(2) & 0xff) << 8 | (b.get(3) & 0xff); + } +} diff --git a/server/src/main/java/itmo2018/se/ClientInfo.java b/server/src/main/java/itmo2018/se/ClientInfo.java new file mode 100644 index 0000000..e2c01b2 --- /dev/null +++ b/server/src/main/java/itmo2018/se/ClientInfo.java @@ -0,0 +1,86 @@ +package itmo2018.se; + +import java.nio.channels.SocketChannel; +import java.util.Arrays; +import java.util.Objects; +import java.util.concurrent.Callable; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class ClientInfo { + private Future closeTask; + private byte[] ip; + private int port; + private int sharingPort; + private ScheduledExecutorService closeScheduled; + private SocketChannel channel; + + public ClientInfo(byte[] ip, short port, SocketChannel channel, ScheduledExecutorService closeScheduled) { + this.ip = ip; + this.port = port; + this.channel = channel; + this.closeScheduled = closeScheduled; + closeTask = startTimer(); + } + + public byte[] getIp() { + return ip; + } + + public int getPort() { + return port; + } + + public void updateCloseTask() { + closeTask.cancel(false); + closeTask = startTimer(); + } + + public boolean isOnline() { + return !closeTask.isDone(); + } + + public int getSharingPort() { + return sharingPort; + } + + public void updateSharingPort(int port) { + this.sharingPort = port; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ClientInfo that = (ClientInfo) o; + return Arrays.equals(ip, that.ip) && + Objects.equals(port, that.port); + } + + public SocketChannel getChannel() { + return channel; + } + + public void disconect() { + closeTask.cancel(false); + } + + @Override + public int hashCode() { + return Objects.hash(ip, port); + } + + private Future startTimer() { + return closeScheduled.schedule(new Closer(), 6, TimeUnit.MINUTES); + } + + private class Closer implements Callable { + @Override + public Void call() throws Exception { + System.out.println("channel is interupt"); + channel.close(); + return null; + } + } +} diff --git a/server/src/main/java/itmo2018/se/Executor.java b/server/src/main/java/itmo2018/se/Executor.java new file mode 100644 index 0000000..ca7b550 --- /dev/null +++ b/server/src/main/java/itmo2018/se/Executor.java @@ -0,0 +1,128 @@ +package itmo2018.se; + +import java.io.*; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Iterator; +import java.util.concurrent.Callable; + +public class Executor implements Callable { + private DataInputStream content; + private ClientDataHolder client; + private FileManager fileManager; + private Writer writer; + + public Executor(byte[] bytes, ClientDataHolder client, FileManager fileManager, Writer writer) { + this.content = new DataInputStream(new ByteArrayInputStream(bytes)); + this.client = client; + this.fileManager = fileManager; + this.writer = writer; + } + + @Override + public Void call() throws Exception { + byte cmd = content.readByte(); + switch (cmd) { + case 1: + executeList(); + break; + case 2: + executeUpload(); + break; + case 3: + executeSources(); + break; + case 4: + executeUpdate(); + break; + } + writer.registerClient(client); + return null; + } + + private void executeList() throws IOException { + System.out.println("list"); + + ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); + DataOutputStream out = new DataOutputStream(byteStream); + out.writeInt(fileManager.filesNumber()); + for (FileInfo file : fileManager) { + out.writeInt(file.getId()); + out.writeUTF(file.getName()); + out.writeLong(file.getSize()); + } + + ByteBuffer response = ByteBuffer.allocate(out.size()); + response.put(byteStream.toByteArray()); + response.flip(); + client.addResponse(response); + } + + private void executeUpload() throws IOException { + System.out.println("upload"); + + String name = content.readUTF(); + long size = content.readLong(); + + System.out.println(name + " " + size); + int id = fileManager.registerFile(name, size, client.getClientInfo()); + + ByteBuffer response = ByteBuffer.allocate(4); + response.putInt(id); + response.flip(); + client.addResponse(response); + } + + private void executeSources() throws IOException { + System.out.println("sources"); + + int id = content.readInt(); + ByteBuffer response; + if (id < 0 || id >= fileManager.filesNumber()) { + response = ByteBuffer.allocate(4); + response.putInt(0); + } else { + FileInfo file = fileManager.getFile(id); + int ownersNumber = file.hasOwner(client.getClientInfo()) ? + file.ownersNumber() - 1 : file.ownersNumber(); + response = ByteBuffer.allocate(4 + ownersNumber * (4 + 2)); + response.putInt(ownersNumber); + for (Iterator it = file.owners(); it.hasNext(); ) { + ClientInfo clientInfo = it.next(); + if (client.getClientInfo().equals(clientInfo)) { + continue; + } + response.put(clientInfo.getIp()); + response.putShort((short) clientInfo.getSharingPort()); + } + } + response.flip(); + client.addResponse(response); + } + + private void executeUpdate() throws IOException { + System.out.println("update"); + + ClientInfo clientInfo = client.getClientInfo(); + clientInfo.updateCloseTask(); + int port = shortToInt(content.readShort()); + client.getClientInfo().updateSharingPort(port); + int count = content.readInt(); + for (int i = 0; i < count; i++) { + int fileId = content.readInt(); + fileManager.getFile(fileId).addOwner(clientInfo); + } + + ByteBuffer response = ByteBuffer.allocate(1); + response.put((byte) 1); + response.flip(); + client.addResponse(response); + } + + private int shortToInt(short s) { + if (s >= 0) { + return s; + } + return 32768 + 32768 + s; + } +} diff --git a/server/src/main/java/itmo2018/se/FileInfo.java b/server/src/main/java/itmo2018/se/FileInfo.java new file mode 100644 index 0000000..c41112a --- /dev/null +++ b/server/src/main/java/itmo2018/se/FileInfo.java @@ -0,0 +1,49 @@ +package itmo2018.se; + +import java.util.*; + +public class FileInfo { + private int id; + private String name; + private long size; + private Set owners = new HashSet<>(); + + public FileInfo(int id, String name, long size, ClientInfo owner) { + this(id, name, size); + this.owners.add(owner); + } + + public FileInfo(int id, String name, long size) { + this.id = id; + this.name = name; + this.size = size; + } + + public void addOwner(ClientInfo client) { + owners.add(client); + } + + public int getId() { + return id; + } + + public String getName() { + return name; + } + + public long getSize() { + return size; + } + + public int ownersNumber() { + return owners.size(); + } + + public boolean hasOwner(ClientInfo clientInfo) { + return owners.contains(clientInfo); + } + + public Iterator owners() { + return owners.iterator(); + } +} diff --git a/server/src/main/java/itmo2018/se/FileManager.java b/server/src/main/java/itmo2018/se/FileManager.java new file mode 100644 index 0000000..ce00143 --- /dev/null +++ b/server/src/main/java/itmo2018/se/FileManager.java @@ -0,0 +1,13 @@ +package itmo2018.se; + +public interface FileManager extends Iterable { + FileInfo getFile(int id); + + int registerFile(String name, long size, ClientInfo owner); + + void setFile(int id, String name, long size); + + void addOwner(int fileId, ClientInfo client); + + int filesNumber(); +} diff --git a/server/src/main/java/itmo2018/se/ListFileManager.java b/server/src/main/java/itmo2018/se/ListFileManager.java new file mode 100644 index 0000000..e673c15 --- /dev/null +++ b/server/src/main/java/itmo2018/se/ListFileManager.java @@ -0,0 +1,55 @@ +package itmo2018.se; + +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +public class ListFileManager implements FileManager { + private List files = new ArrayList<>(); + + @Override + public FileInfo getFile(int id) { + FileInfo file = files.get(id); + for (Iterator it = file.owners(); it.hasNext(); ) { + ClientInfo client = it.next(); + if (!client.isOnline()) { + it.remove(); + } + } + return file; + } + + @Override + public int registerFile(String name, long size, ClientInfo owner) { + int id = files.size(); + files.add(new FileInfo(id, name, size, owner)); + try (OutputStream metaData = new FileOutputStream("metadata", true)) { + metaData.write((id + "\t" + name + "\t" + size + "\n").getBytes()); + } catch (Exception e) { + System.out.println("can't write id to metadata file"); + } + return id; + } + + @Override + public void setFile(int id, String name, long size) { + files.add(new FileInfo(id, name, size)); + } + + @Override + public void addOwner(int fileId, ClientInfo client) { + files.get(fileId).addOwner(client); + } + + @Override + public int filesNumber() { + return files.size(); + } + + @Override + public Iterator iterator() { + return files.iterator(); + } +} diff --git a/server/src/main/java/itmo2018/se/Server.java b/server/src/main/java/itmo2018/se/Server.java new file mode 100644 index 0000000..5f78545 --- /dev/null +++ b/server/src/main/java/itmo2018/se/Server.java @@ -0,0 +1,110 @@ +package itmo2018.se; + +import java.io.File; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.nio.file.Files; +import java.util.Iterator; +import java.util.Set; +import java.util.concurrent.*; + +public class Server { + public static void main(String[] args) throws IOException { + if (args.length != 1) { + System.out.println("server pass one argument: server host"); + return; + } + new Server().run(args[0]); + } + + public Server() throws IOException { + File metaData = new File("metadata"); + if (!metaData.exists()) { + metaData.createNewFile(); + } + Files.lines(metaData.toPath()).map(it -> it.split("\t")) + .forEach(it -> fileManager.setFile(Integer.parseInt(it[0]), it[1], Long.parseLong(it[2]))); + } + + private Selector selector; + private ServerSocketChannel serverSocketChannel; + private ScheduledExecutorService closer = Executors.newScheduledThreadPool(1); + private FileManager fileManager = new ListFileManager(); + + public void run(String adress) throws IOException { + serverSocketChannel = ServerSocketChannel.open(); + InetSocketAddress socketAddress = new InetSocketAddress(adress, 8081); + try { + serverSocketChannel.bind(socketAddress); + } catch (Exception e) { + System.out.println("can't bind adress"); + return; + } + System.out.println("server adress: " + serverSocketChannel.socket().getLocalSocketAddress()); + selector = Selector.open(); + Selector writerSelector = Selector.open(); + Writer writer = new Writer(writerSelector); + Thread writerThread = new Thread(writer); + writerThread.setDaemon(true); + writerThread.start(); + + serverSocketChannel.configureBlocking(false); + serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); + ExecutorService pool = Executors.newFixedThreadPool(8); + + while (selector.select() > -1) { + Set selectedKeys = selector.selectedKeys(); + Iterator keyIterator = selectedKeys.iterator(); + while (keyIterator.hasNext()) { + SelectionKey key = keyIterator.next(); + if (key.isAcceptable()) { + registerClient(); + } else if (key.isReadable()) { + SocketChannel channel = (SocketChannel) key.channel(); + ClientDataHolder client = (ClientDataHolder) key.attachment(); + ByteBuffer buffer = ByteBuffer.allocate(1024); + try { + if (channel.read(buffer) == -1) { + System.out.println(channel.getRemoteAddress() + " is closed"); + client.getClientInfo().disconect(); + key.cancel(); + channel.close(); + } + buffer.flip(); + client.read(buffer); + } catch (IOException e) { + key.cancel(); + System.out.println("client is already disconect"); + } + while (client.requestIsReady()) { + pool.submit(new Executor(client.getRequest(), client, fileManager, writer)); + client.resetRequest(); + client.read(buffer); + } + } + keyIterator.remove(); + } + } + } + + private void registerClient() throws IOException { + System.out.println("Clients number:" + selector.keys().size()); + SocketChannel channel = serverSocketChannel.accept(); + + byte[] ip = channel.socket().getInetAddress().getAddress(); + short port = (short) channel.socket().getPort(); + ClientInfo info = new ClientInfo(ip, port, channel, closer); + + System.out.println(channel.socket().getRemoteSocketAddress()); + + channel.configureBlocking(false); + channel.register(selector, SelectionKey.OP_READ, new ClientDataHolder(info)); + System.out.println("new client"); + } +} diff --git a/server/src/main/java/itmo2018/se/Writer.java b/server/src/main/java/itmo2018/se/Writer.java new file mode 100644 index 0000000..ccf7ed9 --- /dev/null +++ b/server/src/main/java/itmo2018/se/Writer.java @@ -0,0 +1,56 @@ +package itmo2018.se; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; +import java.util.Iterator; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ConcurrentLinkedQueue; + +public class Writer implements Runnable { + private Selector selector; + private Queue clientQueue = new ConcurrentLinkedQueue<>(); + + public Writer(Selector selector) { + this.selector = selector; + } + + public void registerClient(ClientDataHolder client) { + clientQueue.add(client); + selector.wakeup(); + } + + @Override + public void run() { + try { + while (selector.select() > -1) { + if (clientQueue.size() > 0) { + ClientDataHolder data = clientQueue.poll(); + data.getClientInfo().getChannel().register(selector, SelectionKey.OP_WRITE, data); + continue; + } + Set selectedKeys = selector.selectedKeys(); + Iterator keyIterator = selectedKeys.iterator(); + while (keyIterator.hasNext()) { + SelectionKey key = keyIterator.next(); + if (key.isWritable()) { + SocketChannel channel = (SocketChannel) key.channel(); + ClientDataHolder responceQueue = (ClientDataHolder) key.attachment(); + ByteBuffer responce = responceQueue.getResponse(); + if (responce != null) { + channel.write(responce); + } else { + key.cancel(); + } + } + keyIterator.remove(); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/server/src/main/main.iml b/server/src/main/main.iml new file mode 100644 index 0000000..908ad4f --- /dev/null +++ b/server/src/main/main.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file