├── .gitignore ├── LICENSE ├── redis_server.py └── resp.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Kind Jeff 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /redis_server.py: -------------------------------------------------------------------------------- 1 | from socketserver import TCPServer, StreamRequestHandler 2 | from resp import handle_line 3 | 4 | 5 | class RedisHandler(StreamRequestHandler): 6 | def handle(self): 7 | state = None 8 | while True: 9 | data = self.rfile.readline().strip() 10 | print(data) 11 | state = handle_line(data, state) 12 | if state.is_stoped: 13 | break 14 | self.wfile.write(b'+OK\r\n') 15 | print('end') 16 | 17 | 18 | if __name__ == '__main__': 19 | host, port = 'localhost', 6379 20 | with TCPServer((host, port), RedisHandler) as server: 21 | server.serve_forever() 22 | -------------------------------------------------------------------------------- /resp.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | 4 | ANY = lambda v: True # noqa 5 | NUMBER = lambda v: v.isdigit and int(v) >= 0 # noqa 6 | NONZORE_NUMBER = lambda v: v.isdigit and int(v) > 0 # noqa 7 | N_NOT_ZORE = object() 8 | N_IS_ZORE = object() 9 | 10 | 11 | _STATES = ('S', 'A', 'B', 'C', 'D', 'E', 'END') 12 | STATES = namedtuple('STATES', _STATES)(*_STATES) 13 | _STATE_MAP = { 14 | # (FROM_STATE, inputs): TO_STATE 15 | (STATES.S, ('+', '-', ':')): STATES.A, 16 | (STATES.S, ('$',)): STATES.B, 17 | (STATES.S, ('*',)): STATES.D, 18 | (STATES.A, (ANY,)): STATES.END, 19 | (STATES.B, ('-1',)): STATES.END, 20 | (STATES.B, (NUMBER,)): STATES.C, 21 | (STATES.C, (ANY,)): STATES.END, 22 | (STATES.D, ('0', '-1')): STATES.END, 23 | (STATES.D, (NONZORE_NUMBER,)): STATES.E, 24 | (STATES.E, (N_NOT_ZORE,)): STATES.E, # n != 0, n-- 25 | (STATES.E, (N_IS_ZORE,)): STATES.END, # n == 0 26 | } 27 | STATE_MAP = {} 28 | for k, v in _STATE_MAP.items(): 29 | from_state, inputs = k 30 | to_state = v 31 | for i in inputs: 32 | STATE_MAP.setdefault(from_state, {}).update({i: to_state}) 33 | 34 | 35 | class RESPState: 36 | 37 | def __init__(self): 38 | self.lines = [] 39 | self._state = STATES.S 40 | 41 | @property 42 | def state(self): 43 | return self._state 44 | 45 | @state.setter 46 | def state(self, v): 47 | if v not in STATE_MAP[self._state].values(): 48 | raise ValueError('cannot change state from {} to {}'.format( 49 | self._state, v)) 50 | self._state = v 51 | 52 | @property 53 | def is_stoped(self): 54 | return self.state == STATES.END 55 | 56 | def add(self, line): 57 | self.lines.append(line.decode() if isinstance(line, bytes) else line) 58 | self._handle_state() 59 | 60 | def _handle_state(self): 61 | if self.state == STATES.S: 62 | self._handle_state_s() 63 | return 64 | if self.state == STATES.E: 65 | self._handle_state_e() 66 | return 67 | last_input = self.lines[-1] 68 | state_map = STATE_MAP[self.state] 69 | for except_input in state_map.keys(): 70 | if except_input == last_input or (callable(except_input) and 71 | except_input(last_input)): 72 | self.state = state_map[except_input] 73 | return 74 | raise ValueError('state {} not excpet input {}'.format(self.state, 75 | last_input)) 76 | 77 | def _handle_state_s(self): 78 | last_input = self.lines.pop() 79 | symbol, last_input = last_input[0], last_input[1:] 80 | self.lines.extend([symbol, last_input]) 81 | state_map = STATE_MAP[STATES.S] 82 | self.state = state_map[symbol] 83 | self._handle_state() 84 | 85 | def _handle_state_e(self): 86 | if not hasattr(self, 'resp_state') or self.resp_state.is_stoped: 87 | self.resp_state = self.__class__() 88 | # if no n, -2nd input is n 89 | self.n = getattr(self, 'n', None) or int(self.lines[-2]) 90 | self.resp_state.add(self.lines[-1]) 91 | if self.resp_state.state == STATES.END: 92 | self.n -= 1 93 | if self.n == 0: 94 | self.state = STATES.END 95 | elif self.n < 0: 96 | raise ValueError 97 | 98 | 99 | def handle_line(line, resp=None): 100 | if not resp or resp.is_stoped: 101 | resp = RESPState() 102 | resp.add(line) 103 | return resp 104 | --------------------------------------------------------------------------------